This commit is contained in:
Luiz Vasconcelos 2024-10-26 12:27:18 +02:00
parent 589e680b65
commit f6e68fcfab
12 changed files with 493 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
### Go template
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

11
.idea/GitlabLint.xml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.github.blarc.gitlab.template.lint.plugin.settings.ProjectSettings">
<option name="gitlabUrls">
<set>
<option value="https://gitlab.com/api/v4" />
</set>
</option>
<option name="newProject" value="false" />
</component>
</project>

9
.idea/cauth.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/cauth.iml" filepath="$PROJECT_DIR$/.idea/cauth.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

49
auth.go Normal file
View File

@ -0,0 +1,49 @@
package cauth
import (
"context"
"fmt"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
type Params struct {
ClientID string
ClientSecret string
RedirectURL string
AWSRegion string
UserPoolID string
}
type Authenticator struct {
Provider *oidc.Provider
OAuth2Config *oauth2.Config
Verifier *oidc.IDTokenVerifier
}
func NewAuthenticator(ctx context.Context, params Params) (*Authenticator, error) {
issuer := fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s", params.AWSRegion, params.UserPoolID)
provider, err := oidc.NewProvider(ctx, issuer)
if err != nil {
return nil, fmt.Errorf("failed to get provider: %v", err)
}
oauth2Config := &oauth2.Config{
ClientID: params.ClientID,
ClientSecret: params.ClientSecret,
RedirectURL: params.RedirectURL,
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
verifier := provider.Verifier(&oidc.Config{
ClientID: params.ClientID,
})
return &Authenticator{
Provider: provider,
OAuth2Config: oauth2Config,
Verifier: verifier,
}, nil
}

28
go.mod Normal file
View File

@ -0,0 +1,28 @@
module luiz.tec.br/cauth
go 1.23
require (
github.com/coreos/go-oidc/v3 v3.11.0
github.com/gorilla/sessions v1.4.0
github.com/lestrrat-go/jwx v1.2.30
github.com/rbcervilla/redisstore/v9 v9.0.0
github.com/redis/go-redis/v9 v9.7.0
golang.org/x/oauth2 v0.23.0
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.25.0 // indirect
)

61
go.sum Normal file
View File

@ -0,0 +1,61 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.2.30 h1:VKIFrmjYn0z2J51iLPadqoHIVLzvWNa1kCsTqNDHYPA=
github.com/lestrrat-go/jwx v1.2.30/go.mod h1:vMxrwFhunGZ3qddmfmEm2+uced8MSI6QFWGTKygjSzQ=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rbcervilla/redisstore/v9 v9.0.0 h1:wOPbBaydbdxzi1gTafDftCI/Z7vnsXw0QDPCuhiMG0g=
github.com/rbcervilla/redisstore/v9 v9.0.0/go.mod h1:q/acLpoKkTZzIsBYt0R4THDnf8W/BH6GjQYvxDSSfdI=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

152
handlers.go Normal file
View File

@ -0,0 +1,152 @@
package cauth
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"net/http"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
type Handlers struct {
oauth2Config *oauth2.Config
session SessionStorer
verifier *oidc.IDTokenVerifier
}
func NewHandler(oauth2Config *oauth2.Config, session SessionStorer, verifier *oidc.IDTokenVerifier) (*Handlers, error) {
return &Handlers{
oauth2Config: oauth2Config,
session: session,
verifier: verifier,
}, nil
}
type UserClaims struct {
Email string `json:"email"`
Verified bool `json:"email_verified"`
Name string `json:"given_name"`
Username string `json:"cognito:username"`
Picture string `json:"picture"`
}
func generateState() (string, error) {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
func (h *Handlers) SignIn(w http.ResponseWriter, r *http.Request) {
state, err := generateState()
if err != nil {
log.Println("Failed to generate state")
http.Error(w, "Something went wrong", http.StatusInternalServerError)
return
}
session, err := h.session.Get(r)
if err != nil {
http.Error(w, "Failed to get session", http.StatusInternalServerError)
return
}
session.Values["state"] = state
err = session.Save(r, w)
if err != nil {
http.Error(w, "Failed to save session", http.StatusInternalServerError)
return
}
http.Redirect(w, r, h.oauth2Config.AuthCodeURL(state, oauth2.AccessTypeOffline), http.StatusFound)
}
// CallbackHandler handles the OAuth2 callback from Cognito
func (h *Handlers) CallbackHandler(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
session, err := h.session.Get(r)
if err != nil {
http.Error(w, "Failed to get session", http.StatusInternalServerError)
return
}
state, ok := session.Values["state"].(string)
if !ok || state != r.URL.Query().Get("state") {
http.Error(w, "Invalid state parameter", http.StatusBadRequest)
return
}
code := r.URL.Query().Get("code")
if code == "" {
http.Error(w, "Code not found", http.StatusBadRequest)
return
}
oauth2Token, err := h.oauth2Config.Exchange(ctx, code)
if err != nil {
log.Printf("Failed to exchange token: %v", err)
http.Error(w, "Failed to exchange token", http.StatusInternalServerError)
return
}
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
http.Error(w, "No id_token field in oauth2 token", http.StatusInternalServerError)
return
}
idToken, err := h.verifier.Verify(ctx, rawIDToken)
if err != nil {
log.Printf("Failed to verify ID Token: %v", err)
http.Error(w, "Failed to verify ID Token", http.StatusInternalServerError)
return
}
var claims UserClaims
if err := idToken.Claims(&claims); err != nil {
log.Printf("Failed to parse claims: %v", err)
http.Error(w, "Failed to parse claims", http.StatusInternalServerError)
return
}
session.Values["access_token"] = oauth2Token.AccessToken
session.Values["user_info"] = claims
fmt.Println(claims)
session.Options.MaxAge = int(oauth2Token.Expiry.Sub(time.Now()).Seconds())
err = session.Save(r, w)
if err != nil {
log.Printf("Failed to save session: %v", err)
http.Error(w, "Failed to save session", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/", http.StatusFound)
}
// LogoutHandler clears the session and logs the user out
func (h *Handlers) LogoutHandler(w http.ResponseWriter, r *http.Request) {
session, err := h.session.Get(r)
if err != nil {
http.Error(w, "Failed to get session", http.StatusInternalServerError)
return
}
session.Options.MaxAge = -1
err = session.Save(r, w)
if err != nil {
log.Printf("Failed to clear session: %v", err)
http.Error(w, "Failed to clear session", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}

77
middleware.go Normal file
View File

@ -0,0 +1,77 @@
package cauth
import (
"context"
"fmt"
"github.com/lestrrat-go/jwx/jwk"
"log"
"net/http"
)
type contextKey string
const userContextKey = contextKey("user")
type Middleware struct {
s SessionStorer
ck jwk.Set
}
func NewMiddleware(s SessionStorer, cognitoUrl string) *Middleware {
cognitoKeySet, err := jwk.Fetch(context.Background(), cognitoUrl)
if err != nil {
log.Fatal(err)
}
return &Middleware{s, cognitoKeySet}
}
func (m *Middleware) AddUserInfo(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, err := m.s.Get(r)
if err != nil {
next.ServeHTTP(w, r)
return
}
token := session.Values["access_token"]
if token == "" || token == nil {
next.ServeHTTP(w, r)
return
}
userInfo := session.Values["user_info"]
fmt.Println(userInfo)
ctx := context.WithValue(r.Context(), userContextKey, userInfo)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (m *Middleware) ProtectedRoute(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, err := m.s.Get(r)
if err != nil {
http.Redirect(w, r, "/signin", http.StatusSeeOther)
return
}
token := session.Values["access_token"]
if token == "" || token == nil {
http.Redirect(w, r, "/signin", http.StatusSeeOther)
return
}
next.ServeHTTP(w, r)
})
}
func GetUserFromContext(r *http.Request) *UserClaims {
userOptional := r.Context().Value(userContextKey)
if userOptional != nil {
user := userOptional.(UserClaims)
return &user
}
return &UserClaims{}
}

61
session.go Normal file
View File

@ -0,0 +1,61 @@
package cauth
import (
"context"
"encoding/gob"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gorilla/sessions"
"github.com/rbcervilla/redisstore/v9"
"github.com/redis/go-redis/v9"
"golang.org/x/oauth2"
"log"
"net/http"
)
const SESSION_NAME = "auth-session"
type RedisSession struct {
store *redisstore.RedisStore
}
type RedisSessionParams struct {
RedisAddress string
RedisPassword string
//SessionSecret []byte
}
type SessionStorer interface {
Get(r *http.Request) (*sessions.Session, error)
}
func NewRedisSessionStore(params RedisSessionParams) (SessionStorer, error) {
gob.Register(&oauth2.Token{})
gob.Register(oidc.IDToken{})
gob.Register(UserClaims{})
client := redis.NewClient(&redis.Options{
Addr: params.RedisAddress,
Password: params.RedisPassword,
})
store, err := redisstore.NewRedisStore(context.Background(), client)
if err != nil {
log.Fatal("failed to create redis store: ", err)
}
store.KeyPrefix("session_")
store.Options(sessions.Options{
Path: "/",
MaxAge: 86400 * 7,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
return &RedisSession{
store: store,
}, nil
}
func (s *RedisSession) Get(r *http.Request) (*sessions.Session, error) {
return s.store.Get(r, SESSION_NAME)
}