diff --git a/dashboard/src/constants.ts b/dashboard/src/constants.ts index 1ed810d..e4737e5 100644 --- a/dashboard/src/constants.ts +++ b/dashboard/src/constants.ts @@ -2,6 +2,7 @@ export const LOGO_URL = 'https://user-images.githubusercontent.com/6964334/147834043-fc384cab-e7ca-40f8-9663-38fc25fd5f3a.png'; export const TextInputType = { + ACCESS_TOKEN_EXPIRY_TIME: 'ACCESS_TOKEN_EXPIRY_TIME', CLIENT_ID: 'CLIENT_ID', GOOGLE_CLIENT_ID: 'GOOGLE_CLIENT_ID', GITHUB_CLIENT_ID: 'GITHUB_CLIENT_ID', @@ -125,4 +126,5 @@ export interface envVarTypes { DATABASE_NAME: string; DATABASE_TYPE: string; DATABASE_URL: string; + ACCESS_TOKEN_EXPIRY_TIME: string; } diff --git a/dashboard/src/graphql/queries/index.ts b/dashboard/src/graphql/queries/index.ts index a7d0142..1adf02c 100644 --- a/dashboard/src/graphql/queries/index.ts +++ b/dashboard/src/graphql/queries/index.ts @@ -53,6 +53,7 @@ export const EnvVariablesQuery = ` DATABASE_NAME, DATABASE_TYPE, DATABASE_URL, + ACCESS_TOKEN_EXPIRY_TIME, } } `; diff --git a/dashboard/src/pages/Environment.tsx b/dashboard/src/pages/Environment.tsx index b4fd091..c51b95a 100644 --- a/dashboard/src/pages/Environment.tsx +++ b/dashboard/src/pages/Environment.tsx @@ -85,6 +85,7 @@ export default function Environment() { DATABASE_NAME: '', DATABASE_TYPE: '', DATABASE_URL: '', + ACCESS_TOKEN_EXPIRY_TIME: '', }); const [fieldVisibility, setFieldVisibility] = React.useState< @@ -600,19 +601,35 @@ export default function Environment() { - Custom Access Token Scripts + Access Token -
+ + Access Token Expiry Time: + + + + + + + Custom Access Token Scripts: + + + -
+
diff --git a/server/constants/env.go b/server/constants/env.go index f391750..d9af456 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -21,6 +21,8 @@ const ( // EnvKeyPort key for env variable PORT EnvKeyPort = "PORT" + // EnvKeyAccessTokenExpiryTime key for env variable ACCESS_TOKEN_EXPIRY_TIME + EnvKeyAccessTokenExpiryTime = "ACCESS_TOKEN_EXPIRY_TIME" // EnvKeyAdminSecret key for env variable ADMIN_SECRET EnvKeyAdminSecret = "ADMIN_SECRET" // EnvKeyDatabaseType key for env variable DATABASE_TYPE diff --git a/server/env/env.go b/server/env/env.go index 8770441..728bb6b 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -120,6 +120,10 @@ func InitAllEnv() error { } } + if envData.StringEnv[constants.EnvKeyAccessTokenExpiryTime] == "" { + envData.StringEnv[constants.EnvKeyAccessTokenExpiryTime] = os.Getenv(constants.EnvKeyAccessTokenExpiryTime) + } + if envData.StringEnv[constants.EnvKeyAdminSecret] == "" { envData.StringEnv[constants.EnvKeyAdminSecret] = os.Getenv(constants.EnvKeyAdminSecret) } diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 44523e8..e33e660 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -53,6 +53,7 @@ type ComplexityRoot struct { } Env struct { + AccessTokenExpiryTime func(childComplexity int) int AdminSecret func(childComplexity int) int AllowedOrigins func(childComplexity int) int AppURL func(childComplexity int) int @@ -299,6 +300,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AuthResponse.User(childComplexity), true + case "Env.ACCESS_TOKEN_EXPIRY_TIME": + if e.complexity.Env.AccessTokenExpiryTime == nil { + break + } + + return e.complexity.Env.AccessTokenExpiryTime(childComplexity), true + case "Env.ADMIN_SECRET": if e.complexity.Env.AdminSecret == nil { break @@ -1381,6 +1389,7 @@ type Response { } type Env { + ACCESS_TOKEN_EXPIRY_TIME: String ADMIN_SECRET: String DATABASE_NAME: String! DATABASE_URL: String! @@ -1432,6 +1441,7 @@ type GenerateJWTKeysResponse { } input UpdateEnvInput { + ACCESS_TOKEN_EXPIRY_TIME: String ADMIN_SECRET: String CUSTOM_ACCESS_TOKEN_SCRIPT: String OLD_ADMIN_SECRET: String @@ -2221,6 +2231,38 @@ func (ec *executionContext) _AuthResponse_user(ctx context.Context, field graphq return ec.marshalOUser2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUser(ctx, field.Selections, res) } +func (ec *executionContext) _Env_ACCESS_TOKEN_EXPIRY_TIME(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Env", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.AccessTokenExpiryTime, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + func (ec *executionContext) _Env_ADMIN_SECRET(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -8093,6 +8135,14 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob for k, v := range asMap { switch k { + case "ACCESS_TOKEN_EXPIRY_TIME": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ACCESS_TOKEN_EXPIRY_TIME")) + it.AccessTokenExpiryTime, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } case "ADMIN_SECRET": var err error @@ -8711,6 +8761,8 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Env") + case "ACCESS_TOKEN_EXPIRY_TIME": + out.Values[i] = ec._Env_ACCESS_TOKEN_EXPIRY_TIME(ctx, field, obj) case "ADMIN_SECRET": out.Values[i] = ec._Env_ADMIN_SECRET(ctx, field, obj) case "DATABASE_NAME": diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index ea3b5bd..09468b4 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -24,6 +24,7 @@ type DeleteUserInput struct { } type Env struct { + AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"` AdminSecret *string `json:"ADMIN_SECRET"` DatabaseName string `json:"DATABASE_NAME"` DatabaseURL string `json:"DATABASE_URL"` @@ -179,6 +180,7 @@ type UpdateAccessInput struct { } type UpdateEnvInput struct { + AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"` AdminSecret *string `json:"ADMIN_SECRET"` CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"` OldAdminSecret *string `json:"OLD_ADMIN_SECRET"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index a4f29f9..841ad8c 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -87,6 +87,7 @@ type Response { } type Env { + ACCESS_TOKEN_EXPIRY_TIME: String ADMIN_SECRET: String DATABASE_NAME: String! DATABASE_URL: String! @@ -138,6 +139,7 @@ type GenerateJWTKeysResponse { } input UpdateEnvInput { + ACCESS_TOKEN_EXPIRY_TIME: String ADMIN_SECRET: String CUSTOM_ACCESS_TOKEN_SCRIPT: String OLD_ADMIN_SECRET: String diff --git a/server/handlers/authorize.go b/server/handlers/authorize.go index cfb913f..b70c4de 100644 --- a/server/handlers/authorize.go +++ b/server/handlers/authorize.go @@ -5,6 +5,7 @@ import ( "net/http" "strconv" "strings" + "time" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie" @@ -279,7 +280,11 @@ func AuthorizeHandler() gin.HandlerFunc { sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) cookie.SetSession(gc, authToken.FingerPrintHash) - expiresIn := int64(1800) + + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } // used of query mode params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token diff --git a/server/handlers/oauth_callback.go b/server/handlers/oauth_callback.go index c9563a6..bfa4f00 100644 --- a/server/handlers/oauth_callback.go +++ b/server/handlers/oauth_callback.go @@ -157,7 +157,12 @@ func OAuthCallbackHandler() gin.HandlerFunc { if err != nil { c.JSON(500, gin.H{"error": err.Error()}) } - expiresIn := int64(1800) + + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + stateValue + "&id_token=" + authToken.IDToken.Token cookie.SetSession(c, authToken.FingerPrintHash) diff --git a/server/handlers/token.go b/server/handlers/token.go index 13abbb9..516aafe 100644 --- a/server/handlers/token.go +++ b/server/handlers/token.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "net/http" "strings" + "time" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie" @@ -174,7 +175,11 @@ func TokenHandler() gin.HandlerFunc { sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) cookie.SetSession(gc, authToken.FingerPrintHash) - expiresIn := int64(1800) + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + res := map[string]interface{}{ "access_token": authToken.AccessToken.Token, "id_token": authToken.IDToken.Token, diff --git a/server/handlers/verify_email.go b/server/handlers/verify_email.go index 80fe6ad..6333620 100644 --- a/server/handlers/verify_email.go +++ b/server/handlers/verify_email.go @@ -82,7 +82,12 @@ func VerifyEmailHandler() gin.HandlerFunc { c.JSON(500, errorRes) return } - expiresIn := int64(1800) + + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token cookie.SetSession(c, authToken.FingerPrintHash) diff --git a/server/resolvers/env.go b/server/resolvers/env.go index d56ea89..9ee3c60 100644 --- a/server/resolvers/env.go +++ b/server/resolvers/env.go @@ -27,6 +27,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) { // get clone of store store := envstore.EnvStoreObj.GetEnvStoreClone() + accessTokenExpiryTime := store.StringEnv[constants.EnvKeyAccessTokenExpiryTime] adminSecret := store.StringEnv[constants.EnvKeyAdminSecret] clientID := store.StringEnv[constants.EnvKeyClientID] clientSecret := store.StringEnv[constants.EnvKeyClientSecret] @@ -67,6 +68,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) { organizationLogo := store.StringEnv[constants.EnvKeyOrganizationLogo] res = &model.Env{ + AccessTokenExpiryTime: &accessTokenExpiryTime, AdminSecret: &adminSecret, DatabaseName: databaseName, DatabaseURL: databaseURL, diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 57f8158..a93ca8d 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "strings" + "time" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie" @@ -73,7 +74,11 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes return res, err } - expiresIn := int64(1800) + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + res = &model.AuthResponse{ Message: `Logged in successfully`, AccessToken: &authToken.AccessToken.Token, diff --git a/server/resolvers/session.go b/server/resolvers/session.go index e68fe07..22e7171 100644 --- a/server/resolvers/session.go +++ b/server/resolvers/session.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log" + "time" "github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/db" @@ -73,7 +74,11 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) cookie.SetSession(gc, authToken.FingerPrintHash) - expiresIn := int64(1800) + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + res = &model.AuthResponse{ Message: `Session token refreshed`, AccessToken: &authToken.AccessToken.Token, diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index 76d7175..317416e 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -176,7 +176,10 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR cookie.SetSession(gc, authToken.FingerPrintHash) go utils.SaveSessionInDB(gc, user.ID) - expiresIn := int64(1800) + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } res = &model.AuthResponse{ Message: `Signed up successfully.`, diff --git a/server/resolvers/verify_email.go b/server/resolvers/verify_email.go index fe13c9a..65e8494 100644 --- a/server/resolvers/verify_email.go +++ b/server/resolvers/verify_email.go @@ -64,7 +64,11 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m cookie.SetSession(gc, authToken.FingerPrintHash) go utils.SaveSessionInDB(gc, user.ID) - expiresIn := int64(1800) + expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() + if expiresIn <= 0 { + expiresIn = 1 + } + res = &model.AuthResponse{ Message: `Email verified successfully.`, AccessToken: &authToken.AccessToken.Token, diff --git a/server/token/auth_token.go b/server/token/auth_token.go index 8714792..905df4a 100644 --- a/server/token/auth_token.go +++ b/server/token/auth_token.go @@ -130,7 +130,11 @@ func CreateRefreshToken(user models.User, roles, scopes []string, hostname, nonc // CreateAccessToken util to create JWT token, based on // user information, roles config and CUSTOM_ACCESS_TOKEN_SCRIPT func CreateAccessToken(user models.User, roles, scopes []string, hostName, nonce string) (string, int64, error) { - expiryBound := time.Minute * 30 + expiryBound, err := utils.ParseDurationInSeconds(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAccessTokenExpiryTime)) + if err != nil { + expiryBound = time.Minute * 15 + } + expiresAt := time.Now().Add(expiryBound).Unix() customClaims := jwt.MapClaims{ @@ -282,7 +286,11 @@ func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionD // 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 + expiryBound, err := utils.ParseDurationInSeconds(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAccessTokenExpiryTime)) + if err != nil { + expiryBound = time.Minute * 15 + } + expiresAt := time.Now().Add(expiryBound).Unix() resUser := user.AsAPIUser() diff --git a/server/utils/parser.go b/server/utils/parser.go new file mode 100644 index 0000000..1b037c0 --- /dev/null +++ b/server/utils/parser.go @@ -0,0 +1,21 @@ +package utils + +import ( + "errors" + "time" +) + +// ParseDurationInSeconds parses input s, removes ms/us/ns and returns result duration +func ParseDurationInSeconds(s string) (time.Duration, error) { + d, err := time.ParseDuration(s) + if err != nil { + return 0, err + } + + d = d.Truncate(time.Second) + if d <= 0 { + return 0, errors.New(`duration must be greater than 0s`) + } + + return d, nil +}