fix: auth flow

This commit is contained in:
Lakhan Samani
2022-03-02 17:42:31 +05:30
parent 5399ea8f32
commit f0f2e0b6c8
47 changed files with 786 additions and 972 deletions

View File

@@ -2,23 +2,23 @@ package token
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"strings"
"time"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/crypto"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/google/uuid"
"github.com/robertkrimen/otto"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/crypto"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/utils"
)
// JWTToken is a struct to hold JWT token and its expiration time
@@ -33,60 +33,219 @@ type Token struct {
FingerPrintHash string `json:"fingerprint_hash"`
RefreshToken *JWTToken `json:"refresh_token"`
AccessToken *JWTToken `json:"access_token"`
IDToken *JWTToken `json:"id_token"`
}
// SessionData
type SessionData struct {
Subject string `json:"sub"`
Roles []string `json:"roles"`
Scope []string `json:"scope"`
Nonce string `json:"nonce"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
}
// CreateSessionToken creates a new session token
func CreateSessionToken(user models.User, nonce string, roles, scope []string) (*SessionData, string, error) {
fingerPrintMap := &SessionData{
Nonce: nonce,
Roles: roles,
Subject: user.ID,
Scope: scope,
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().AddDate(1, 0, 0).Unix(),
}
fingerPrintBytes, _ := json.Marshal(fingerPrintMap)
fingerPrintHash, err := crypto.EncryptAES(string(fingerPrintBytes))
if err != nil {
return nil, "", err
}
return fingerPrintMap, fingerPrintHash, nil
}
// CreateAuthToken creates a new auth token when userlogs in
func CreateAuthToken(user models.User, roles []string) (*Token, error) {
fingerprint := uuid.NewString()
fingerPrintHashBytes, err := crypto.EncryptAES([]byte(fingerprint))
func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string) (*Token, error) {
hostname := utils.GetHost(gc)
nonce := uuid.New().String()
_, fingerPrintHash, err := CreateSessionToken(user, nonce, roles, scope)
if err != nil {
return nil, err
}
refreshToken, refreshTokenExpiresAt, err := CreateRefreshToken(user, roles)
accessToken, accessTokenExpiresAt, err := CreateAccessToken(user, roles, scope, hostname, nonce)
if err != nil {
return nil, err
}
accessToken, accessTokenExpiresAt, err := CreateAccessToken(user, roles)
idToken, idTokenExpiresAt, err := CreateIDToken(user, roles, hostname, nonce)
if err != nil {
return nil, err
}
return &Token{
FingerPrint: fingerprint,
FingerPrintHash: string(fingerPrintHashBytes),
RefreshToken: &JWTToken{Token: refreshToken, ExpiresAt: refreshTokenExpiresAt},
res := &Token{
FingerPrint: nonce,
FingerPrintHash: fingerPrintHash,
AccessToken: &JWTToken{Token: accessToken, ExpiresAt: accessTokenExpiresAt},
}, nil
IDToken: &JWTToken{Token: idToken, ExpiresAt: idTokenExpiresAt},
}
if utils.StringSliceContains(scope, "offline_access") {
refreshToken, refreshTokenExpiresAt, err := CreateRefreshToken(user, roles, hostname, nonce)
if err != nil {
return nil, err
}
res.RefreshToken = &JWTToken{Token: refreshToken, ExpiresAt: refreshTokenExpiresAt}
}
return res, nil
}
// CreateRefreshToken util to create JWT token
func CreateRefreshToken(user models.User, roles []string) (string, int64, error) {
func CreateRefreshToken(user models.User, roles []string, hostname, nonce string) (string, int64, error) {
// expires in 1 year
expiryBound := time.Hour * 8760
expiresAt := time.Now().Add(expiryBound).Unix()
customClaims := jwt.MapClaims{
"iss": "",
"aud": "",
"iss": hostname,
"aud": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID),
"sub": user.ID,
"exp": expiresAt,
"iat": time.Now().Unix(),
"token_type": constants.TokenTypeRefreshToken,
"roles": roles,
"id": user.ID,
"nonce": nonce,
}
token, err := SignJWTToken(customClaims)
if err != nil {
return "", 0, err
}
return token, expiresAt, nil
}
// CreateAccessToken util to create JWT token, based on
// user information, roles config and CUSTOM_ACCESS_TOKEN_SCRIPT
func CreateAccessToken(user models.User, roles []string) (string, int64, error) {
func CreateAccessToken(user models.User, roles, scopes []string, hostName, nonce string) (string, int64, error) {
expiryBound := time.Minute * 30
expiresAt := time.Now().Add(expiryBound).Unix()
customClaims := jwt.MapClaims{
"iss": hostName,
"aud": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID),
"nonce": nonce,
"sub": user.ID,
"exp": expiresAt,
"iat": time.Now().Unix(),
"token_type": constants.TokenTypeAccessToken,
"scope": scopes,
"roles": roles,
}
token, err := SignJWTToken(customClaims)
if err != nil {
return "", 0, err
}
return token, expiresAt, nil
}
// GetAccessToken returns the access token from the request (either from header or cookie)
func GetAccessToken(gc *gin.Context) (string, error) {
// try to check in auth header for cookie
auth := gc.Request.Header.Get("Authorization")
if auth == "" {
return "", fmt.Errorf(`unauthorized`)
}
if !strings.HasPrefix(auth, "Bearer ") {
return "", fmt.Errorf(`not a bearer token`)
}
token := strings.TrimPrefix(auth, "Bearer ")
return token, nil
}
// Function to validate access token for authorizer apis (profile, update_profile)
func ValidateAccessToken(gc *gin.Context, accessToken string) (map[string]interface{}, error) {
var res map[string]interface{}
if accessToken == "" {
return res, fmt.Errorf(`unauthorized`)
}
savedSession := sessionstore.GetState(accessToken)
if savedSession == "" {
return res, fmt.Errorf(`unauthorized`)
}
savedSessionSplit := strings.Split(savedSession, "@")
nonce := savedSessionSplit[0]
userID := savedSessionSplit[1]
hostname := utils.GetHost(gc)
res, err := ParseJWTToken(accessToken, hostname, nonce, userID)
if err != nil {
return res, err
}
if res["token_type"] != constants.TokenTypeAccessToken {
return res, fmt.Errorf(`unauthorized: invalid token type`)
}
return res, nil
}
func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionData, error) {
if encryptedSession == "" {
return nil, fmt.Errorf(`unauthorized`)
}
savedSession := sessionstore.GetState(encryptedSession)
if savedSession == "" {
return nil, fmt.Errorf(`unauthorized`)
}
savedSessionSplit := strings.Split(savedSession, "@")
nonce := savedSessionSplit[0]
userID := savedSessionSplit[1]
decryptedFingerPrint, err := crypto.DecryptAES(encryptedSession)
if err != nil {
return nil, err
}
var res SessionData
err = json.Unmarshal([]byte(decryptedFingerPrint), &res)
if err != nil {
return nil, err
}
if res.Nonce != nonce {
return nil, fmt.Errorf(`unauthorized: invalid nonce`)
}
if res.Subject != userID {
return nil, fmt.Errorf(`unauthorized: invalid user id`)
}
if res.ExpiresAt < time.Now().Unix() {
return nil, fmt.Errorf(`unauthorized: token expired`)
}
// TODO validate scope
// if !reflect.DeepEqual(res.Roles, roles) {
// return res, "", fmt.Errorf(`unauthorized`)
// }
return &res, nil
}
// CreateIDToken util to create JWT token, based on
// user information, roles config and CUSTOM_ACCESS_TOKEN_SCRIPT
func CreateIDToken(user models.User, roles []string, hostname, nonce string) (string, int64, error) {
expiryBound := time.Minute * 30
expiresAt := time.Now().Add(expiryBound).Unix()
@@ -97,13 +256,13 @@ func CreateAccessToken(user models.User, roles []string) (string, int64, error)
claimKey := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtRoleClaim)
customClaims := jwt.MapClaims{
"iss": "",
"iss": hostname,
"aud": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID),
"nonce": "",
"nonce": nonce,
"sub": user.ID,
"exp": expiresAt,
"iat": time.Now().Unix(),
"token_type": constants.TokenTypeAccessToken,
"token_type": constants.TokenTypeIdentityToken,
"allowed_roles": strings.Split(user.Roles, ","),
claimKey: roles,
}
@@ -152,58 +311,18 @@ func CreateAccessToken(user models.User, roles []string) (string, int64, error)
return token, expiresAt, nil
}
// GetAccessToken returns the access token from the request (either from header or cookie)
func GetAccessToken(gc *gin.Context) (string, error) {
token, err := cookie.GetAccessTokenCookie(gc)
if err != nil || token == "" {
// try to check in auth header for cookie
auth := gc.Request.Header.Get("Authorization")
if auth == "" {
return "", fmt.Errorf(`unauthorized`)
}
token = strings.TrimPrefix(auth, "Bearer ")
}
return token, nil
}
// GetRefreshToken returns the refresh token from cookie / request query url
func GetRefreshToken(gc *gin.Context) (string, error) {
token, err := cookie.GetRefreshTokenCookie(gc)
if err != nil || token == "" {
// GetIDToken returns the id token from the request header
func GetIDToken(gc *gin.Context) (string, error) {
// try to check in auth header for cookie
auth := gc.Request.Header.Get("Authorization")
if auth == "" {
return "", fmt.Errorf(`unauthorized`)
}
if !strings.HasPrefix(auth, "Bearer ") {
return "", fmt.Errorf(`not a bearer token`)
}
token := strings.TrimPrefix(auth, "Bearer ")
return token, nil
}
// GetFingerPrint returns the finger print from cookie
func GetFingerPrint(gc *gin.Context) (string, error) {
fingerPrint, err := cookie.GetFingerPrintCookie(gc)
if err != nil || fingerPrint == "" {
return "", fmt.Errorf(`no finger print`)
}
return fingerPrint, nil
}
func ValidateAccessToken(gc *gin.Context) (map[string]interface{}, error) {
token, err := GetAccessToken(gc)
if err != nil {
return nil, err
}
claims, err := ParseJWTToken(token)
if err != nil {
return nil, err
}
// also validate if there is user session present with access token
sessions := sessionstore.GetUserSessions(claims["id"].(string))
if len(sessions) == 0 {
return nil, errors.New("unauthorized")
}
return claims, nil
}

View File

@@ -44,7 +44,7 @@ func SignJWTToken(claims jwt.MapClaims) (string, error) {
}
// ParseJWTToken common util to parse jwt token
func ParseJWTToken(token string) (jwt.MapClaims, error) {
func ParseJWTToken(token, hostname, nonce, subject string) (jwt.MapClaims, error) {
jwtType := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtType)
signingMethod := jwt.GetSigningMethod(jwtType)
@@ -87,5 +87,21 @@ func ParseJWTToken(token string) (jwt.MapClaims, error) {
claims["exp"] = intExp
claims["iat"] = intIat
if claims["aud"] != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) {
return claims, errors.New("invalid audience")
}
if claims["nonce"] != nonce {
return claims, errors.New("invalid nonce")
}
if claims["iss"] != hostname {
return claims, errors.New("invalid issuer")
}
if claims["sub"] != subject {
return claims, errors.New("invalid subject")
}
return claims, nil
}

View File

@@ -9,13 +9,15 @@ import (
)
// CreateVerificationToken creates a verification JWT token
func CreateVerificationToken(email, tokenType, hostname string) (string, error) {
func CreateVerificationToken(email, tokenType, hostname, nonceHash string) (string, error) {
claims := jwt.MapClaims{
"iss": hostname,
"aud": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID),
"sub": email,
"exp": time.Now().Add(time.Minute * 30).Unix(),
"iat": time.Now().Unix(),
"token_type": tokenType,
"email": email,
"host": hostname,
"nonce": nonceHash,
"redirect_url": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL),
}