From f8859c5312e9ed1c357feccbe6b0b0207fc88636 Mon Sep 17 00:00:00 2001 From: Luiz Vasconcelos Date: Fri, 13 Sep 2024 21:39:02 +0200 Subject: [PATCH] Initial Commit --- go.mod | 23 ++++ go.sum | 28 +++++ pkg/cognito/client.go | 138 ++++++++++++++++++++++ pkg/cognito/handlers.go | 246 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 435 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/cognito/client.go create mode 100644 pkg/cognito/handlers.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0458f83 --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module cognito-auth + +go 1.23.0 + +require ( + github.com/aws/aws-sdk-go-v2 v1.30.5 + github.com/aws/aws-sdk-go-v2/config v1.27.33 + github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.43.4 +) + +require ( + github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect + github.com/aws/smithy-go v1.20.4 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1825c38 --- /dev/null +++ b/go.sum @@ -0,0 +1,28 @@ +github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g= +github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= +github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU= +github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks= +github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I= +github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.43.4 h1:C8uf+nwieFWZtdPTCYOM8u/UyaIsDPfr95TJrfYekwQ= +github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.43.4/go.mod h1:hsciKQ2xFfOPEuebyKmFo7wOSVNoLuzmCi6Qtol4UDc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 h1:pIaGg+08llrP7Q5aiz9ICWbY8cqhTkyy+0SHvfzQpTc= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.7/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1KvQzkzPyiKUdlWJqy+J4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o= +github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= +github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= diff --git a/pkg/cognito/client.go b/pkg/cognito/client.go new file mode 100644 index 0000000..1339af9 --- /dev/null +++ b/pkg/cognito/client.go @@ -0,0 +1,138 @@ +package cognito + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "os" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider" + "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider/types" +) + +type Client struct { + CognitoClient *cognitoidentityprovider.Client + poolId string + clientId string +} + +func NewAuthClient(poolID string, clientId string) (AuthClient, error) { + sdkConfig, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + fmt.Println(err) + return nil, err + } + cognitoClient := cognitoidentityprovider.NewFromConfig(sdkConfig) + + return &Client{CognitoClient: cognitoClient, poolId: poolID, clientId: clientId}, nil +} + +type AuthClient interface { + SignUp(req SignUpRequest) error + ConfirmSignUp(req ConfirmSignUpRequest) error + SignIn(ctx context.Context, username, password string) (*AuthenticationResult, error) +} + +type SignUpRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + Email string `json:"email" binding:"required,email"` + Context context.Context +} + +func (c *Client) SignUp(req SignUpRequest) error { + secretHash := getSecretHash(req.Username, *&c.clientId) + params := &cognitoidentityprovider.SignUpInput{ + ClientId: aws.String(c.clientId), + Username: aws.String(req.Username), + Password: aws.String(req.Password), + UserAttributes: []types.AttributeType{ + {Name: aws.String("email"), Value: aws.String(req.Email)}, + }, + SecretHash: &secretHash, + } + + _, err := c.CognitoClient.SignUp(context.Background(), params) + if err != nil { + return nil + } + return err +} + +type ConfirmSignUpRequest struct { + Context context.Context + Username string + ConfirmationCode string +} + +func (c *Client) ConfirmSignUp(req ConfirmSignUpRequest) error { + input := &cognitoidentityprovider.ConfirmSignUpInput{ + ClientId: aws.String(c.clientId), + Username: aws.String(req.Username), + ConfirmationCode: aws.String(req.ConfirmationCode), + } + + _, err := c.CognitoClient.ConfirmSignUp(req.Context, input) + if err != nil { + return err + } + + return nil +} + +type SignInRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// SignIn authenticates a user and returns tokens +func (c *Client) SignIn(ctx context.Context, username, password string) (*AuthenticationResult, error) { + authParams := map[string]string{ + "USERNAME": username, + "PASSWORD": password, + } + + input := &cognitoidentityprovider.InitiateAuthInput{ + AuthFlow: types.AuthFlowTypeUserPasswordAuth, + ClientId: aws.String(c.clientId), + AuthParameters: authParams, + } + + output, err := c.CognitoClient.InitiateAuth(ctx, input) + if err != nil { + return nil, err + } + + if output.AuthenticationResult == nil { + return nil, errors.New("authentication result is nil") + } + + return &AuthenticationResult{ + IDToken: aws.ToString(output.AuthenticationResult.IdToken), + AccessToken: aws.ToString(output.AuthenticationResult.AccessToken), + RefreshToken: aws.ToString(output.AuthenticationResult.RefreshToken), + }, nil +} + +// AuthenticationResult holds the tokens returned after successful authentication +type AuthenticationResult struct { + IDToken string + AccessToken string + RefreshToken string +} + +func getSecretHash(username string, clientID string) string { + secret := os.Getenv("COGNITO_SECRET") + if secret == "" { + secret = "1bb1r4fegke1hcn6rjo8d38io5np0qcce7juhjb8hu4kvu6qfr3s" + } + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(fmt.Sprintf("%s%s", username, clientID))) + + return base64.StdEncoding.EncodeToString(mac.Sum(nil)) +} diff --git a/pkg/cognito/handlers.go b/pkg/cognito/handlers.go new file mode 100644 index 0000000..7550f42 --- /dev/null +++ b/pkg/cognito/handlers.go @@ -0,0 +1,246 @@ +package cognito + +import ( + "encoding/json" + "html/template" + "net/http" + "path/filepath" +) + +type Handlers struct { + Client *Client + Templates *template.Template +} + +func NewHandlers(client *Client, templateDir string) (*Handlers, error) { + templates, err := template.ParseGlob(filepath.Join(templateDir, "*.html")) + if err != nil { + return nil, err + } + + return &Handlers{ + Client: client, + Templates: templates, + }, nil +} + +func (h *Handlers) RenderTemplate(w http.ResponseWriter, tmpl string, data interface{}) { + err := h.Templates.ExecuteTemplate(w, tmpl+".html", data) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} + +type JSONResponse struct { + Message string `json:"message,omitempty"` + Data interface{} `json:"data,omitempty"` + Error string `json:"error,omitempty"` +} + +func (h *Handlers) SignUpHTMLHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + // Render the sign-up form + h.RenderTemplate(w, "signup", nil) + return + } + + if r.Method == http.MethodPost { + // Parse form data + if err := r.ParseForm(); err != nil { + h.RenderTemplate(w, "signup", map[string]string{"Error": "Invalid form data"}) + return + } + + username := r.FormValue("username") + email := r.FormValue("email") + password := r.FormValue("password") + + // Sign up the user + err := h.Client.SignUp(SignUpRequest{ + Context: r.Context(), + Username: username, + Password: password, + Email: email, + }) + if err != nil { + h.RenderTemplate(w, "signup", map[string]string{"Error": err.Error()}) + return + } + + // Redirect to confirmation page or show success message + http.Redirect(w, r, "/confirm_signup", http.StatusSeeOther) + return + } + + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) +} + +func (h *Handlers) SignUpJSONHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + var req struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + } + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&req); err != nil { + http.Error(w, "Bad Request: "+err.Error(), http.StatusBadRequest) + return + } + + err := h.Client.SignUp(SignUpRequest{ + Username: req.Username, + Password: req.Password, + Email: req.Email, + Context: r.Context(), + }) + + if err != nil { + response := JSONResponse{Error: err.Error()} + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(response) + return + } + + response := JSONResponse{Message: "User signed up successfully. Please confirm your email."} + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + +func (h *Handlers) ConfirmSignUpHTMLHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + // Render the confirm sign-up form + h.RenderTemplate(w, "confirm_signup", nil) + return + } + + if r.Method == http.MethodPost { + // Parse form data + if err := r.ParseForm(); err != nil { + h.RenderTemplate(w, "confirm_signup", map[string]string{"Error": "Invalid form data"}) + return + } + + username := r.FormValue("username") + confirmationCode := r.FormValue("confirmation_code") + + // Confirm sign-up + err := h.Client.ConfirmSignUp(ConfirmSignUpRequest{ + Context: r.Context(), + Username: username, + ConfirmationCode: confirmationCode, + }) + if err != nil { + h.RenderTemplate(w, "confirm_signup", map[string]string{"Error": err.Error()}) + return + } + + // Redirect to sign-in page or show success message + http.Redirect(w, r, "/signin", http.StatusSeeOther) + return + } + + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) +} + +func (h *Handlers) ConfirmSignUpJSONHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + var req struct { + Username string `json:"username"` + ConfirmationCode string `json:"confirmation_code"` + } + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&req); err != nil { + http.Error(w, "Bad Request: "+err.Error(), http.StatusBadRequest) + return + } + + err := h.Client.ConfirmSignUp(ConfirmSignUpRequest{ + Context: r.Context(), + Username: req.Username, + ConfirmationCode: req.ConfirmationCode, + }) + if err != nil { + response := JSONResponse{Error: err.Error()} + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(response) + return + } + + response := JSONResponse{Message: "User confirmed successfully."} + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + +// SignInHTMLHandler handles HTML sign-in requests +func (h *Handlers) SignInHTMLHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + // Render the sign-in form + h.RenderTemplate(w, "signin", nil) + return + } + + if r.Method == http.MethodPost { + // Parse form data + if err := r.ParseForm(); err != nil { + h.RenderTemplate(w, "signin", map[string]string{"Error": "Invalid form data"}) + return + } + + username := r.FormValue("username") + password := r.FormValue("password") + + // Sign in the user + authResult, err := h.Client.SignIn(r.Context(), username, password) + if err != nil { + h.RenderTemplate(w, "signin", map[string]string{"Error": err.Error()}) + return + } + + // Render a success page or display tokens + h.RenderTemplate(w, "signin_success", authResult) + return + } + + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) +} + +func (h *Handlers) SignInJSONHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + var req struct { + Username string `json:"username"` + Password string `json:"password"` + } + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&req); err != nil { + http.Error(w, "Bad Request: "+err.Error(), http.StatusBadRequest) + return + } + + authResult, err := h.Client.SignIn(r.Context(), req.Username, req.Password) + if err != nil { + response := JSONResponse{Error: err.Error()} + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(response) + return + } + + response := JSONResponse{Data: authResult} + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +}