moved auth into middleware
This commit is contained in:
@@ -44,18 +44,19 @@ type (
|
|||||||
|
|
||||||
// AccessTokenClaims is the claims Auth0 provides in access tokens
|
// AccessTokenClaims is the claims Auth0 provides in access tokens
|
||||||
AccessTokenClaims struct {
|
AccessTokenClaims struct {
|
||||||
Audience string `json:"aud"`
|
Audience string `json:"aud"` // TODO: fill in from db
|
||||||
Expires int64 `json:"exp"`
|
Expires int64 `json:"exp"`
|
||||||
|
Expiration time.Time `json:"-"` // parsed Expires
|
||||||
FamilyName string `json:"family_name"`
|
FamilyName string `json:"family_name"`
|
||||||
GivenName string `json:"given_name"`
|
GivenName string `json:"given_name"`
|
||||||
IssuedAt int64 `json:"iat"`
|
IssuedAt int64 `json:"iat"` // TODO: fill in from db
|
||||||
Issuer string `json:"iss"`
|
Issuer string `json:"iss"` // TODO: fill in from db
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Picture string `json:"picture"`
|
Picture string `json:"picture"`
|
||||||
SessionID string `json:"sid"`
|
SessionID string `json:"sid"` // TODO: fill in from db
|
||||||
Subject string `json:"sub"`
|
Subject string `json:"sub"` // TODO: fill in from db
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"` // TODO: fill in from db
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func (a *Authenticator) DeleteOAuthTokens(ctx context.Context, accessToken strin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, accessToken string) (claims AccessTokenClaims, expiration time.Time, err error) {
|
func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, accessToken string) (claims AccessTokenClaims, err error) {
|
||||||
rows, err := a.db.Query(
|
rows, err := a.db.Query(
|
||||||
ctx,
|
ctx,
|
||||||
`
|
`
|
||||||
@@ -119,7 +119,7 @@ func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, a
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to perform query: %w", err)
|
return AccessTokenClaims{}, fmt.Errorf("failed to perform query: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Row struct {
|
type Row struct {
|
||||||
@@ -135,11 +135,13 @@ func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, a
|
|||||||
r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row])
|
r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return AccessTokenClaims{}, time.Time{}, consts.ErrNotFound
|
return AccessTokenClaims{}, consts.ErrNotFound
|
||||||
}
|
}
|
||||||
return AccessTokenClaims{}, time.Time{}, fmt.Errorf("failed to scan row: %w", err)
|
return AccessTokenClaims{}, fmt.Errorf("failed to scan row: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
claims.Expires = r.Expiry.Unix()
|
||||||
|
claims.Expiration = r.Expiry
|
||||||
claims.Name = r.Id_token_custom_claims_name
|
claims.Name = r.Id_token_custom_claims_name
|
||||||
claims.Picture = r.Id_token_custom_claims_picture
|
claims.Picture = r.Id_token_custom_claims_picture
|
||||||
claims.Nickname = r.Id_token_custom_claims_nickname
|
claims.Nickname = r.Id_token_custom_claims_nickname
|
||||||
@@ -147,7 +149,7 @@ func (a *Authenticator) GetAccessTokenClaimsAndExpiration(ctx context.Context, a
|
|||||||
claims.FamilyName = r.Id_token_custom_claims_family_name
|
claims.FamilyName = r.Id_token_custom_claims_family_name
|
||||||
claims.UpdatedAt = r.Id_token_custom_claims_updated_at
|
claims.UpdatedAt = r.Id_token_custom_claims_updated_at
|
||||||
|
|
||||||
return claims, r.Expiry, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) getRefreshTokenForAccessToken(ctx context.Context, accessToken string) (refreshToken, tokenType string, err error) {
|
func (a *Authenticator) getRefreshTokenForAccessToken(ctx context.Context, accessToken string) (refreshToken, tokenType string, err error) {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ func Routes(
|
|||||||
r gin.IRouter,
|
r gin.IRouter,
|
||||||
logger *logging.Logger,
|
logger *logging.Logger,
|
||||||
sq *sse.Queue,
|
sq *sse.Queue,
|
||||||
auth *middleware.Auth,
|
|
||||||
) {
|
) {
|
||||||
s := &sseRouter{
|
s := &sseRouter{
|
||||||
log: logger,
|
log: logger,
|
||||||
@@ -37,7 +36,7 @@ func Routes(
|
|||||||
users: make(map[string][maxNumOpenConnectionsPerUser]context.CancelFunc),
|
users: make(map[string][maxNumOpenConnectionsPerUser]context.CancelFunc),
|
||||||
}
|
}
|
||||||
|
|
||||||
r.GET("/", auth.AuthenticateAndAddIdentityGin(), response.Handler(s.serveEvents))
|
r.GET("/", response.Handler(s.serveEvents))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *sseRouter) serveEvents(c *gin.Context) (response.Response, error) {
|
func (r *sseRouter) serveEvents(c *gin.Context) (response.Response, error) {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ type (
|
|||||||
rawEvents *raw_events.Store
|
rawEvents *raw_events.Store
|
||||||
accts *accounts.Store
|
accts *accounts.Store
|
||||||
etsy *etsy_platform.Platform
|
etsy *etsy_platform.Platform
|
||||||
authMiddleware *middleware.Auth
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrTemplateNotFound is returned if the reason the template failed to compile
|
// ErrTemplateNotFound is returned if the reason the template failed to compile
|
||||||
@@ -41,12 +40,12 @@ type (
|
|||||||
|
|
||||||
func SetupRoutes(
|
func SetupRoutes(
|
||||||
logger *logging.Logger,
|
logger *logging.Logger,
|
||||||
r gin.IRouter,
|
r gin.IRoutes,
|
||||||
contentDir string,
|
contentDir string,
|
||||||
rawEvents *raw_events.Store,
|
rawEvents *raw_events.Store,
|
||||||
accts *accounts.Store,
|
accts *accounts.Store,
|
||||||
etsy *etsy_platform.Platform,
|
etsy *etsy_platform.Platform,
|
||||||
authMiddleware *middleware.Auth,
|
auth *middleware.Auth,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
s := &webpageRouter{
|
s := &webpageRouter{
|
||||||
@@ -93,31 +92,10 @@ func SetupRoutes(
|
|||||||
rawEvents: rawEvents,
|
rawEvents: rawEvents,
|
||||||
accts: accts,
|
accts: accts,
|
||||||
etsy: etsy,
|
etsy: etsy,
|
||||||
authMiddleware: authMiddleware,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticate := s.authMiddleware.AuthenticateAndAddIdentityToRequest()
|
r.GET("", response.Handler(s.serveTemplate))
|
||||||
|
r.GET("/*rest", response.Handler(auth.Authenticate()), response.Handler(s.serveTemplate))
|
||||||
r.GET("/*rest", response.Handler(func(c *gin.Context) (response.Response, error) {
|
|
||||||
c.Request.URL.Path = c.Request.URL.Path[3:]
|
|
||||||
defer func() {
|
|
||||||
c.Request.URL.Path = "/ui" + c.Request.URL.Path
|
|
||||||
}()
|
|
||||||
|
|
||||||
p := c.Request.URL.Path
|
|
||||||
if p == "/" || p == "" {
|
|
||||||
c, err := s.authMiddleware.AddIdentityToRequest(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return s.serveTemplate(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res, err := authenticate(c); res != nil || err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
return s.serveTemplate(c)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /
|
// GET /
|
||||||
|
|||||||
@@ -52,91 +52,69 @@ func NewAuth(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) AddIdentity(fn response.HandlerFunc) response.HandlerFunc {
|
// AddIdentityToRequest will add an Identity to the context that can then be retrieved via GetIdentity.
|
||||||
return func(c *gin.Context) (response.Response, error) {
|
func (a *Auth) AddIdentityToRequest(c *gin.Context) {
|
||||||
c, err := a.AddIdentityToRequest(c)
|
if err := a.addIdentityToRequest(c); err != nil {
|
||||||
if err != nil {
|
c.Error(err)
|
||||||
return nil, err
|
c.Abort()
|
||||||
}
|
|
||||||
|
|
||||||
return fn(c)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) AddIdentityToRequest(c *gin.Context) (*gin.Context, error) {
|
func (a *Auth) addIdentityToRequest(c *gin.Context) error {
|
||||||
r := c.Request
|
r := c.Request
|
||||||
|
|
||||||
ck, err := r.Cookie("access_token")
|
ck, err := r.Cookie("access_token")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
accessToken := ck.Value
|
accessToken := ck.Value
|
||||||
|
|
||||||
claims, expiration, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
|
claims, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, consts.ErrNotFound) {
|
if errors.Is(err, consts.ErrNotFound) {
|
||||||
return c, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, response.Errorf("failed to load authentication details: %w", err)
|
return response.Errorf("failed to load authentication details: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expiration := claims.Expiration
|
||||||
if expiration.Before(time.Now()) {
|
if expiration.Before(time.Now()) {
|
||||||
return c, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken)
|
user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c, response.Errorf("failed to load user and account defails: %w", err)
|
return response.Errorf("failed to load user and account defails: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Request = r.WithContext(SetIdentity(ctx, Identity{
|
c.Request = r.WithContext(SetIdentity(c, Identity{
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
Claims: claims,
|
Claims: claims,
|
||||||
User: user,
|
User: user,
|
||||||
Account: acct,
|
Account: acct,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return c, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use the new middleware pattern
|
// Authenticate should only be used along with and after AddIdentityToRequest
|
||||||
// auth middleware to verify access_token cookie and set custom claims in the request context
|
// Typically used with response.Handler to make a gin.HandlerFunc.
|
||||||
func (a *Auth) AuthenticateAndAddIdentityGin(assertions ...AuthorizationAssertions) gin.HandlerFunc {
|
func (a *Auth) Authenticate(assertions ...AuthorizationAssertions) func(c *gin.Context) (response.Response, error) {
|
||||||
fn := a.AuthenticateAndAddIdentityToRequest(assertions...)
|
|
||||||
return response.Handler(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Auth) AuthenticateAndAddIdentityToRequest(assertions ...AuthorizationAssertions) response.HandlerFunc {
|
|
||||||
return func(c *gin.Context) (response.Response, error) {
|
return func(c *gin.Context) (response.Response, error) {
|
||||||
r := c.Request
|
id, ok := getIdentity(c)
|
||||||
ck, err := r.Cookie("access_token")
|
if !ok {
|
||||||
if err != nil {
|
return nil, response.Unauthorized().
|
||||||
return response.TemporaryRedirect("/").
|
HTML([]byte(`
|
||||||
JSON("no access_token cookie provided"), nil
|
<h1>Unauthorized</h1>
|
||||||
}
|
<a href="/">Return to app</a>
|
||||||
|
`)) // TODO: would be nice to have a better page for this
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
accessToken := ck.Value
|
|
||||||
|
|
||||||
claims, expiration, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, consts.ErrNotFound) {
|
|
||||||
u, err := a.newLoginURL(ctx, a.auth, r.URL.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, response.Errorf("failed to generate login url: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.TemporaryRedirect(u), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, response.Errorf("failed to authenticate: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expiration := id.Claims.Expiration
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
// refresh tokens, when the access token is "old enough"
|
// refresh tokens, when the access token is "old enough"
|
||||||
@@ -144,7 +122,7 @@ func (a *Auth) AuthenticateAndAddIdentityToRequest(assertions ...AuthorizationAs
|
|||||||
// id token lifetime is 48 hours, allowing a person to use the app everyday comfortably, with wiggle room, without having to log in.
|
// id token lifetime is 48 hours, allowing a person to use the app everyday comfortably, with wiggle room, without having to log in.
|
||||||
const idTokenLifetime = 48 * time.Hour
|
const idTokenLifetime = 48 * time.Hour
|
||||||
if refreshFloor := expiration.Add(-(idTokenLifetime / 4)); refreshFloor.Before(now) {
|
if refreshFloor := expiration.Add(-(idTokenLifetime / 4)); refreshFloor.Before(now) {
|
||||||
accessToken, expiration, err = a.auth.RefreshAccessToken(ctx, accessToken)
|
accessToken, expiration, err := a.auth.RefreshAccessToken(c, id.AccessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.log.Warn("failed to refresh access token", "error", err)
|
a.log.Warn("failed to refresh access token", "error", err)
|
||||||
return response.TemporaryRedirect("/").
|
return response.TemporaryRedirect("/").
|
||||||
@@ -153,65 +131,55 @@ func (a *Auth) AuthenticateAndAddIdentityToRequest(assertions ...AuthorizationAs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 'redirect' to same url, to set the new access_token cookie
|
// 'redirect' to same url, to set the new access_token cookie
|
||||||
return response.TemporaryRedirect(r.URL.String()).
|
return response.TemporaryRedirect(c.Request.URL.String()).
|
||||||
Cookie(cookies.AccessToken(accessToken, expiration)), nil
|
Cookie(cookies.AccessToken(accessToken, expiration)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// add identity info to request context
|
|
||||||
|
|
||||||
user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken)
|
|
||||||
if err != nil {
|
|
||||||
return nil, response.Errorf("failed to authorize: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, as := range assertions {
|
for _, as := range assertions {
|
||||||
res, err := as(c)
|
if res, err := as(c); res != nil || err != nil {
|
||||||
if res != nil || err != nil {
|
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id := Identity{
|
|
||||||
AccessToken: accessToken,
|
|
||||||
Claims: claims,
|
|
||||||
User: user,
|
|
||||||
Account: acct,
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Set(identityKeyString, id)
|
|
||||||
c.Request = r.WithContext(SetIdentity(ctx, id))
|
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type identityKey struct{}
|
type identityKey struct{}
|
||||||
|
|
||||||
const identityKeyString = "identity"
|
|
||||||
|
|
||||||
// stores identity in context
|
// stores identity in context
|
||||||
func SetIdentity(ctx context.Context, id Identity) context.Context {
|
func SetIdentity(ctx context.Context, id Identity) context.Context {
|
||||||
|
c, ok := ctx.(*gin.Context)
|
||||||
|
if ok {
|
||||||
|
c.Set(identityKey{}, id)
|
||||||
|
ctx = c.Request.Context()
|
||||||
|
}
|
||||||
return context.WithValue(ctx, identityKey{}, id)
|
return context.WithValue(ctx, identityKey{}, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get identity from context
|
// get identity from context
|
||||||
func GetIdentity(ctx context.Context) Identity {
|
func GetIdentity(ctx context.Context) Identity {
|
||||||
|
id, _ := getIdentity(ctx)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIdentity(ctx context.Context) (Identity, bool) {
|
||||||
id, ok := ctx.Value(identityKey{}).(Identity)
|
id, ok := ctx.Value(identityKey{}).(Identity)
|
||||||
if ok {
|
if ok {
|
||||||
return id
|
return id, true
|
||||||
}
|
}
|
||||||
|
|
||||||
c, ok := ctx.(*gin.Context)
|
c, ok := ctx.(*gin.Context)
|
||||||
if !ok {
|
if !ok {
|
||||||
return Identity{}
|
return Identity{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
v, ok := c.Get(identityKeyString)
|
v, ok := c.Get(identityKey{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return Identity{}
|
return Identity{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
id, _ = v.(Identity)
|
id, ok = v.(Identity)
|
||||||
|
|
||||||
return id
|
return id, ok
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,11 @@ func NewRouter(
|
|||||||
// kind of a dumb way to capture routes for webpages
|
// kind of a dumb way to capture routes for webpages
|
||||||
templates_api.SetupRoutes(
|
templates_api.SetupRoutes(
|
||||||
logger.WithGroup("templates"),
|
logger.WithGroup("templates"),
|
||||||
r.Group("/ui"),
|
r.Group(
|
||||||
|
"/ui",
|
||||||
|
stripPrefix("/ui"),
|
||||||
|
authMiddleware.AddIdentityToRequest,
|
||||||
|
),
|
||||||
contentDir,
|
contentDir,
|
||||||
rawEvents,
|
rawEvents,
|
||||||
accts,
|
accts,
|
||||||
@@ -107,13 +111,20 @@ func NewRouter(
|
|||||||
auth,
|
auth,
|
||||||
)
|
)
|
||||||
sse_api.Routes(
|
sse_api.Routes(
|
||||||
api.Group("/events"),
|
api.Group(
|
||||||
|
"/events",
|
||||||
|
authMiddleware.AddIdentityToRequest,
|
||||||
|
response.Handler(authMiddleware.Authenticate()),
|
||||||
|
),
|
||||||
apiLogger.WithGroup("/events"),
|
apiLogger.WithGroup("/events"),
|
||||||
sq,
|
sq,
|
||||||
authMiddleware,
|
|
||||||
)
|
)
|
||||||
accounts_api.Routes(
|
accounts_api.Routes(
|
||||||
api.Group("/accounts", authMiddleware.AuthenticateAndAddIdentityGin()),
|
api.Group(
|
||||||
|
"/accounts",
|
||||||
|
authMiddleware.AddIdentityToRequest,
|
||||||
|
response.Handler(authMiddleware.Authenticate()),
|
||||||
|
),
|
||||||
apiLogger.WithGroup("/accounts"),
|
apiLogger.WithGroup("/accounts"),
|
||||||
accts,
|
accts,
|
||||||
unp.Group("/accounts"),
|
unp.Group("/accounts"),
|
||||||
@@ -158,3 +169,18 @@ func fileServer(urlPrefix, dir string, beforeServe func(c *gin.Context)) gin.Han
|
|||||||
// TODO: need a c.Next()?
|
// TODO: need a c.Next()?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stripPrefix(prefix string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if !strings.HasPrefix(c.Request.URL.Path, prefix) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Request.URL.Path = c.Request.URL.Path[len(prefix):]
|
||||||
|
defer func() {
|
||||||
|
c.Request.URL.Path = prefix + c.Request.URL.Path
|
||||||
|
}()
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user