Initial Commit

This commit is contained in:
Luiz Vasconcelos 2024-09-13 21:39:02 +02:00
commit f8859c5312
4 changed files with 435 additions and 0 deletions

23
go.mod Normal file
View File

@ -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
)

28
go.sum Normal file
View File

@ -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=

138
pkg/cognito/client.go Normal file
View File

@ -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))
}

246
pkg/cognito/handlers.go Normal file
View File

@ -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)
}