removed intermediate /internal directory
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"ruben/inventory2/consts"
|
||||
"ruben/inventory2/domains/accounts"
|
||||
"ruben/inventory2/domains/authentication"
|
||||
"ruben/inventory2/logging"
|
||||
"ruben/inventory2/server/cookies"
|
||||
"ruben/inventory2/server/response"
|
||||
)
|
||||
|
||||
type (
|
||||
Auth struct {
|
||||
log *logging.Logger
|
||||
auth *authentication.Authenticator
|
||||
accts *accounts.Store
|
||||
}
|
||||
|
||||
Identity struct {
|
||||
AccessToken string
|
||||
Claims authentication.AccessTokenClaims
|
||||
User accounts.OAuthUser
|
||||
Account *accounts.Account
|
||||
}
|
||||
|
||||
LoginURLProviderFunc = func(ctx context.Context, auth *authentication.Authenticator, targetURI string) (string, error)
|
||||
|
||||
AuthorizationAssertions = response.HandlerFunc
|
||||
)
|
||||
|
||||
func NewAuth(
|
||||
logger *logging.Logger,
|
||||
auth *authentication.Authenticator,
|
||||
accts *accounts.Store,
|
||||
) *Auth {
|
||||
return &Auth{
|
||||
log: logger,
|
||||
auth: auth,
|
||||
accts: accts,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Auth) GetAuthenticator() *authentication.Authenticator {
|
||||
return a.auth
|
||||
}
|
||||
|
||||
// Identify will add an Identity to the context that can then be retrieved via GetIdentity.
|
||||
func (a *Auth) Identify(c *gin.Context) {
|
||||
if err := a.addIdentity(c); err != nil {
|
||||
c.Error(err)
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Auth) addIdentity(c *gin.Context) error {
|
||||
r := c.Request
|
||||
|
||||
ck, err := r.Cookie("access_token")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
accessToken := ck.Value
|
||||
|
||||
claims, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken)
|
||||
if err != nil {
|
||||
if errors.Is(err, consts.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return response.Errorf("failed to load authentication details: %w", err)
|
||||
}
|
||||
|
||||
expiration := claims.Expiration
|
||||
if expiration.Before(time.Now()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken)
|
||||
if err != nil {
|
||||
return response.Errorf("failed to load user and account defails: %w", err)
|
||||
}
|
||||
|
||||
c.Request = r.WithContext(SetIdentity(c, Identity{
|
||||
AccessToken: accessToken,
|
||||
Claims: claims,
|
||||
User: user,
|
||||
Account: acct,
|
||||
}))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate should only be used along with and after Identify
|
||||
// Typically used with response.Handler to make a gin.HandlerFunc.
|
||||
func (a *Auth) Authenticate(assertions ...AuthorizationAssertions) func(c *gin.Context) {
|
||||
return response.Handler(a.AuthenticateHandler(assertions...))
|
||||
}
|
||||
|
||||
// See Authenticate.
|
||||
func (a *Auth) AuthenticateHandler(assertions ...AuthorizationAssertions) func(c *gin.Context) (response.Response, error) {
|
||||
return func(c *gin.Context) (response.Response, error) {
|
||||
id, ok := getIdentity(c)
|
||||
if !ok {
|
||||
return nil, response.Unauthorized().
|
||||
HTML([]byte(`
|
||||
<h1>Unauthorized</h1>
|
||||
<a href="/">Return to app</a>
|
||||
`)) // TODO: would be nice to have a better page for this
|
||||
}
|
||||
|
||||
expiration := id.Claims.Expiration
|
||||
now := time.Now()
|
||||
|
||||
// refresh tokens, when the access token is "old enough"
|
||||
|
||||
// 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
|
||||
if refreshFloor := expiration.Add(-(idTokenLifetime / 4)); refreshFloor.Before(now) {
|
||||
accessToken, expiration, err := a.auth.RefreshAccessToken(c, id.AccessToken)
|
||||
if err != nil {
|
||||
a.log.Warn("failed to refresh access token", "error", err)
|
||||
return response.TemporaryRedirect("/").
|
||||
Body(io.NopCloser(bytes.NewBuffer([]byte(fmt.Sprintf("failed to refresh access token: %v", err))))).
|
||||
Cookie(cookies.Expired("access_token")), nil
|
||||
}
|
||||
|
||||
// 'redirect' to same url, to set the new access_token cookie
|
||||
return response.TemporaryRedirect(c.Request.URL.String()).
|
||||
Cookie(cookies.AccessToken(accessToken, expiration)), nil
|
||||
}
|
||||
|
||||
for _, as := range assertions {
|
||||
if res, err := as(c); res != nil || err != nil {
|
||||
return res, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
type identityKey struct{}
|
||||
|
||||
// stores identity in 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)
|
||||
}
|
||||
|
||||
// get identity from context
|
||||
func GetIdentity(ctx context.Context) Identity {
|
||||
id, _ := getIdentity(ctx)
|
||||
return id
|
||||
}
|
||||
|
||||
func getIdentity(ctx context.Context) (Identity, bool) {
|
||||
id, ok := ctx.Value(identityKey{}).(Identity)
|
||||
if ok {
|
||||
return id, true
|
||||
}
|
||||
|
||||
c, ok := ctx.(*gin.Context)
|
||||
if !ok {
|
||||
return Identity{}, false
|
||||
}
|
||||
|
||||
v, ok := c.Get(identityKey{})
|
||||
if !ok {
|
||||
return Identity{}, false
|
||||
}
|
||||
|
||||
id, ok = v.(Identity)
|
||||
|
||||
return id, ok
|
||||
}
|
||||
Reference in New Issue
Block a user