Skip to content

Commit

Permalink
Update AWS Cognito
Browse files Browse the repository at this point in the history
  • Loading branch information
ledongthuc committed Oct 27, 2021
1 parent b986c37 commit ac01c17
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 4 deletions.
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ Binary will available in folder "./build/". Run it and you can access through ht

AWS Secrets Manager UI tool uses AWS configuration credential to authenticate requests.

### Credential file

More detail: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html

### Credential environment variables (recommend)

More detail: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html

### Credential file

More detail: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html

### Minimum Permission

- Easy policy name: SecretsManagerReadWrite : https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_available-policies.html
Expand All @@ -64,6 +64,28 @@ More detail: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envv

## Authentication

### AWS Cognito authentication

Configurations to enable for AWS Cognito

- `AUTH_ENABLED=true`
- `AUTH_TYPE=aws_cognito_auth2`
- `AWS_COGNITO_APP_NAME=administrator`: Get from AWS Cognito App configuration
- `AWS_COGNITO_REGION=eu-north-1`: Get from AWS Cognito App configuration
- `AWS_COGNITO_CLIENT_ID={client_id}`: Get from AWS Cognito App configuration
- `AWS_COGNITO_CLIENT_SECRET={secrets}`: Get from AWS Cognito App configuration
- `AWS_COGNITO_REDIRECT_URL=http://localhost:30301/cognito/auth`: Redirect URL you want AWS cognito call back
- `[email protected]`: Limit accepted users to login. Empty = all
- `AWS_COGNITO_LOGIN_URL=https://administrator.auth.eu-north-1.amazoncognito.com/login?...`: Get from AWS Cognito App configuration

AWS Cognito App configurations

![aws_cognito_1](https://user-images.githubusercontent.com/1828895/139128226-5f5b0068-a54e-49b6-80d1-36261476e7d0.png)

![aws_cognito_2](https://user-images.githubusercontent.com/1828895/139128230-bbecb312-3f2e-4fdf-887a-fc089c184ea4.png)

### Basic authenticationBLED

Default, AWS Secrets manager UI disable authentication.

AWS Secrets manager supports basic auth through two variable environments, in order enable it, try with 2 variable environments:
Expand Down
124 changes: 124 additions & 0 deletions server/actions/aws_cognito.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package actions

import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
)

func GetAWSCognitoAppName() string {
return os.Getenv("AWS_COGNITO_APP_NAME")
}

func GetAWSCognitoRegion() string {
return os.Getenv("AWS_COGNITO_REGION")
}

func GetAWSCognitoClientID() string {
return os.Getenv("AWS_COGNITO_CLIENT_ID")
}

func GetAWSCognitoClientSecret() string {
return os.Getenv("AWS_COGNITO_CLIENT_SECRET")
}

func GetAWSCognitoHost() string {
return fmt.Sprintf(
"https://%s.auth.%s.amazoncognito.com",
GetAWSCognitoAppName(),
GetAWSCognitoRegion(),
)
}

type CognitoToken struct {
AccessToken *string `json:"access_token"`
TokenType *string `json:"token_type"`
IDToken *string `json:"id_token"`
ExpiresIn *int64 `json:"expires_in"`
}

func GetAWSCognitoAccessToken(authCode string) (CognitoToken, error) {
urlStr := GetAWSCognitoHost() + "/oauth2/token"
request := url.Values{}
request.Set("grant_type", "authorization_code")
request.Set("client_id", GetAWSCognitoClientID())
request.Set("code", authCode)
request.Set("redirect_uri", os.Getenv("AWS_COGNITO_REDIRECT_URL"))
r, err := http.NewRequest(http.MethodPost, urlStr, strings.NewReader(request.Encode()))
if err != nil {
return CognitoToken{}, err
}
r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
r.Header.Add("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", GetAWSCognitoClientID(), GetAWSCognitoClientSecret())))))
client := &http.Client{}
resp, err := client.Do(r)
if err != nil {
return CognitoToken{}, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return CognitoToken{}, err
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return CognitoToken{}, errors.New(string(body))
}

var token CognitoToken
if err := json.Unmarshal(body, &token); err != nil {
return CognitoToken{}, err
}

return token, nil
}

type CognitoUserInfo struct {
Sub string `json:"sub"`
Name string `json:"name"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
PreferredUsername string `json:"preferred_username"`
Email string `json:"email"`
}

func (c CognitoUserInfo) IsValidEmail() (bool, error) {
emails := os.Getenv("AWS_COGNITO_ALLOWED_EMAILS")
if len(emails) == 0 {
return true, nil
}
for _, email := range strings.Split(emails, ",") {
if email == c.Email {
return true, nil
}
}
return false, fmt.Errorf("%s doesn't have permission to access system", c.Email)
}

func GetAWSCognitoUserInfo(accessToken string) (CognitoUserInfo, error) {
urlStr := GetAWSCognitoHost() + "/oauth2/userInfo"
r, _ := http.NewRequest(http.MethodGet, urlStr, nil)
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
client := &http.Client{}
resp, err := client.Do(r)
if err != nil {
return CognitoUserInfo{}, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return CognitoUserInfo{}, err
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return CognitoUserInfo{}, errors.New(string(body))
}

var userInfo CognitoUserInfo
if err := json.Unmarshal(body, &userInfo); err != nil {
return CognitoUserInfo{}, err
}
return userInfo, nil
}
55 changes: 55 additions & 0 deletions server/auth/aws_cognito.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package auth

import (
"net/http"
"os"
"strings"
"sync"

"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
)

var awsCognitoLoginUsers *sync.Map

func GetAWSCognitoLoginUsers() *sync.Map {
if awsCognitoLoginUsers == nil {
awsCognitoLoginUsers = &sync.Map{}
}
return awsCognitoLoginUsers
}

func AWSCognitoMiddleware(skipPathes []string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
for _, path := range skipPathes {
if strings.HasPrefix(c.Request().URL.Path, path) {
return next(c)
}
}
cookie, err := c.Cookie("aws_cognito_token")
if err != nil {
return awsCognitoError(c, err.Error())
}
if cookie == nil {
return awsCognitoError(c, "Fail to load cookie")
}

_, valid := GetAWSCognitoLoginUsers().Load(cookie.Value)
if !valid {
return awsCognitoError(c, "invalid or expired token")
}

return next(c)
}
}
}

func awsCognitoError(c echo.Context, errMsg string) error {
loginURL := os.Getenv("AWS_COGNITO_LOGIN_URL")
if len(loginURL) == 0 {
return echo.NewHTTPError(http.StatusUnauthorized, errMsg)
}
log.Warn("Unauthentication: ", errMsg)
return c.Redirect(http.StatusTemporaryRedirect, loginURL)
}
10 changes: 10 additions & 0 deletions server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,19 @@ func main() {
if os.Getenv("AUTH_ENABLED") == "true" {
// Temporary, we use auth_basic as default authen method. When login page's implemented, we switch back the login_page as default auth type
if os.Getenv("AUTH_TYPE") == "login_form" {
log.Info("Start with login form")
routes.SetupLoginRoute(e.Group("/api"))
mainGroup.Use(middleware.JWTWithConfig(auth.CreateJWTAuth()))
} else if os.Getenv("AUTH_TYPE") == "aws_cognito_auth2" {
log.Info("Start with AWS Cognito Auth 2.0")
routes.SetupAWSCognitoRoute(e.Group("/cognito"))
mainGroup.Use(auth.AWSCognitoMiddleware([]string{
"/icons",
"/js",
"/css",
}))
} else {
log.Info("Start with basic auth")
e.Use(middleware.BasicAuth(routes.Auth))
}
}
Expand Down
57 changes: 57 additions & 0 deletions server/routes/aws_cognito.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package routes

import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"time"

"github.com/labstack/echo/v4"

"github.com/ledongthuc/awssecretsmanagerui/server/actions"
"github.com/ledongthuc/awssecretsmanagerui/server/auth"
)

func SetupAWSCognitoRoute(g *echo.Group) {
g.GET("/auth", func(c echo.Context) error {
authCode := c.QueryParam("code")
if len(authCode) == 0 {
return c.JSON(http.StatusBadRequest, "missing auth code")
}
token, err := actions.GetAWSCognitoAccessToken(authCode)
if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error())
}

if token.AccessToken == nil {
return c.JSON(http.StatusInternalServerError, fmt.Errorf("Missing access token"))
}
userInfo, err := actions.GetAWSCognitoUserInfo(*token.AccessToken)
if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error())
}

if valid, err := userInfo.IsValidEmail(); !valid {
return c.JSON(http.StatusBadRequest, err.Error())
}

h := sha256.New()
h.Write([]byte(*token.AccessToken))
clientToken := base64.StdEncoding.EncodeToString(h.Sum(nil))

cookie := new(http.Cookie)
cookie.Name = "aws_cognito_token"
cookie.Value = clientToken
cookie.Path = "/"
cookie.Expires = time.Now().Add(24 * time.Hour)
c.SetCookie(cookie)

auth.GetAWSCognitoLoginUsers().Store(clientToken, struct{}{})
return c.HTML(http.StatusOK, `<html>
<head><script>location.href = "/";</script></head>
<body></body>
</html>`)
// return c.Redirect(http.StatusTemporaryRedirect, "/")
})
}

0 comments on commit ac01c17

Please sign in to comment.