From e49e315967ea4dbf5eb2a1bc0d7c337b335298e1 Mon Sep 17 00:00:00 2001 From: scaletech-milan <112945284+scaletech-milan@users.noreply.github.com> Date: Sat, 2 Dec 2023 12:21:53 +0530 Subject: [PATCH 1/3] Feat: Add oauth2 for twitch (#426) * fix: * removed fmt.Println * Feat: - Add OAuth for twitch --------- Co-authored-by: lemonScaletech Co-authored-by: Anand Kumar Panigrahi <70533637+lemonScaletech@users.noreply.github.com> --- .../components/EnvComponents/OAuthConfig.tsx | 39 ++++ dashboard/src/constants.ts | 4 + dashboard/src/graphql/queries/index.ts | 2 + dashboard/src/pages/Environment.tsx | 3 + server/constants/auth_methods.go | 2 + server/constants/env.go | 4 + server/graph/generated/generated.go | 190 +++++++++++++++++- server/graph/model/models_gen.go | 5 + server/graph/schema.graphqls | 5 + server/handlers/oauth_callback.go | 44 +++- server/handlers/oauth_login.go | 24 ++- server/oauth/oauth.go | 35 +++- server/resolvers/env.go | 7 +- server/resolvers/update_env.go | 6 + 14 files changed, 363 insertions(+), 7 deletions(-) diff --git a/dashboard/src/components/EnvComponents/OAuthConfig.tsx b/dashboard/src/components/EnvComponents/OAuthConfig.tsx index 68377b1..663019f 100644 --- a/dashboard/src/components/EnvComponents/OAuthConfig.tsx +++ b/dashboard/src/components/EnvComponents/OAuthConfig.tsx @@ -17,6 +17,7 @@ import { FaApple, FaTwitter, FaMicrosoft, + FaTwitch, } from 'react-icons/fa'; import { TextInputType, @@ -397,6 +398,44 @@ const OAuthConfig = ({ /> + +
+ +
+
+ +
+
+ +
+
diff --git a/dashboard/src/constants.ts b/dashboard/src/constants.ts index f45200d..04b1d4c 100644 --- a/dashboard/src/constants.ts +++ b/dashboard/src/constants.ts @@ -12,6 +12,7 @@ export const TextInputType = { TWITTER_CLIENT_ID: 'TWITTER_CLIENT_ID', MICROSOFT_CLIENT_ID: 'MICROSOFT_CLIENT_ID', MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID: 'MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID', + TWITCH_CLIENT_ID: 'TWITCH_CLIENT_ID', JWT_ROLE_CLAIM: 'JWT_ROLE_CLAIM', REDIS_URL: 'REDIS_URL', SMTP_HOST: 'SMTP_HOST', @@ -42,6 +43,7 @@ export const HiddenInputType = { APPLE_CLIENT_SECRET: 'APPLE_CLIENT_SECRET', TWITTER_CLIENT_SECRET: 'TWITTER_CLIENT_SECRET', MICROSOFT_CLIENT_SECRET: 'MICROSOFT_CLIENT_SECRET', + TWITCH_CLIENT_SECRET: 'TWITCH_CLIENT_SECRET', JWT_SECRET: 'JWT_SECRET', SMTP_PASSWORD: 'SMTP_PASSWORD', ADMIN_SECRET: 'ADMIN_SECRET', @@ -132,6 +134,8 @@ export interface envVarTypes { MICROSOFT_CLIENT_ID: string; MICROSOFT_CLIENT_SECRET: string; MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID: string; + TWITCH_CLIENT_ID: string; + TWITCH_CLIENT_SECRET: string; ROLES: [string] | []; DEFAULT_ROLES: [string] | []; PROTECTED_ROLES: [string] | []; diff --git a/dashboard/src/graphql/queries/index.ts b/dashboard/src/graphql/queries/index.ts index ffa8cd9..cd31871 100644 --- a/dashboard/src/graphql/queries/index.ts +++ b/dashboard/src/graphql/queries/index.ts @@ -35,6 +35,8 @@ export const EnvVariablesQuery = ` MICROSOFT_CLIENT_ID MICROSOFT_CLIENT_SECRET MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID + TWITCH_CLIENT_ID + TWITCH_CLIENT_SECRET DEFAULT_ROLES PROTECTED_ROLES ROLES diff --git a/dashboard/src/pages/Environment.tsx b/dashboard/src/pages/Environment.tsx index 8871f4a..33b11a4 100644 --- a/dashboard/src/pages/Environment.tsx +++ b/dashboard/src/pages/Environment.tsx @@ -55,6 +55,8 @@ const Environment = () => { MICROSOFT_CLIENT_ID: '', MICROSOFT_CLIENT_SECRET: '', MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID: '', + TWITCH_CLIENT_ID: '', + TWITCH_CLIENT_SECRET: '', ROLES: [], DEFAULT_ROLES: [], PROTECTED_ROLES: [], @@ -107,6 +109,7 @@ const Environment = () => { LINKEDIN_CLIENT_SECRET: false, APPLE_CLIENT_SECRET: false, TWITTER_CLIENT_SECRET: false, + TWITCH_CLIENT_SECRET: false, JWT_SECRET: false, SMTP_PASSWORD: false, ADMIN_SECRET: false, diff --git a/server/constants/auth_methods.go b/server/constants/auth_methods.go index dbe5175..3372d26 100644 --- a/server/constants/auth_methods.go +++ b/server/constants/auth_methods.go @@ -23,4 +23,6 @@ const ( AuthRecipeMethodTwitter = "twitter" // AuthRecipeMethodMicrosoft is the microsoft auth method AuthRecipeMethodMicrosoft = "microsoft" + // AuthRecipeMethodTwitch is the twitch auth method + AuthRecipeMethodTwitch = "twitch" ) diff --git a/server/constants/env.go b/server/constants/env.go index e89984b..1def063 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -118,6 +118,10 @@ const ( EnvKeyMicrosoftActiveDirectoryTenantID = "MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID" // EnvKeyMicrosoftClientSecret key for env variable MICROSOFT_CLIENT_SECRET EnvKeyMicrosoftClientSecret = "MICROSOFT_CLIENT_SECRET" + // EnvKeyTwitchClientID key for env variable TWITCH_CLIENT_ID + EnvKeyTwitchClientID = "TWITCH_CLIENT_ID" + // EnvKeyTwitchClientSecret key for env variable TWITCH_CLIENT_SECRET + EnvKeyTwitchClientSecret = "TWITCH_CLIENT_SECRET" // EnvKeyOrganizationName key for env variable ORGANIZATION_NAME EnvKeyOrganizationName = "ORGANIZATION_NAME" // EnvKeyOrganizationLogo key for env variable ORGANIZATION_LOGO diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index b59b34d..594b5b9 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -139,6 +139,8 @@ type ComplexityRoot struct { SMTPUsername func(childComplexity int) int SenderEmail func(childComplexity int) int SenderName func(childComplexity int) int + TwitchClientID func(childComplexity int) int + TwitchClientSecret func(childComplexity int) int TwitterClientID func(childComplexity int) int TwitterClientSecret func(childComplexity int) int } @@ -173,6 +175,7 @@ type ComplexityRoot struct { IsMultiFactorAuthEnabled func(childComplexity int) int IsSignUpEnabled func(childComplexity int) int IsStrongPasswordEnabled func(childComplexity int) int + IsTwitchLoginEnabled func(childComplexity int) int IsTwitterLoginEnabled func(childComplexity int) int Version func(childComplexity int) int } @@ -992,6 +995,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Env.SenderName(childComplexity), true + case "Env.TWITCH_CLIENT_ID": + if e.complexity.Env.TwitchClientID == nil { + break + } + + return e.complexity.Env.TwitchClientID(childComplexity), true + + case "Env.TWITCH_CLIENT_SECRET": + if e.complexity.Env.TwitchClientSecret == nil { + break + } + + return e.complexity.Env.TwitchClientSecret(childComplexity), true + case "Env.TWITTER_CLIENT_ID": if e.complexity.Env.TwitterClientID == nil { break @@ -1146,6 +1163,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Meta.IsStrongPasswordEnabled(childComplexity), true + case "Meta.is_twitch_login_enabled": + if e.complexity.Meta.IsTwitchLoginEnabled == nil { + break + } + + return e.complexity.Meta.IsTwitchLoginEnabled(childComplexity), true + case "Meta.is_twitter_login_enabled": if e.complexity.Meta.IsTwitterLoginEnabled == nil { break @@ -2324,6 +2348,7 @@ type Meta { is_apple_login_enabled: Boolean! is_twitter_login_enabled: Boolean! is_microsoft_login_enabled: Boolean! + is_twitch_login_enabled: Boolean! is_email_verification_enabled: Boolean! is_basic_authentication_enabled: Boolean! is_magic_link_login_enabled: Boolean! @@ -2477,6 +2502,8 @@ type Env { MICROSOFT_CLIENT_ID: String MICROSOFT_CLIENT_SECRET: String MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID: String + TWITCH_CLIENT_ID: String + TWITCH_CLIENT_SECRET: String ORGANIZATION_NAME: String ORGANIZATION_LOGO: String APP_COOKIE_SECURE: Boolean! @@ -2604,6 +2631,8 @@ input UpdateEnvInput { MICROSOFT_CLIENT_ID: String MICROSOFT_CLIENT_SECRET: String MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID: String + TWITCH_CLIENT_ID: String + TWITCH_CLIENT_SECRET: String ORGANIZATION_NAME: String ORGANIZATION_LOGO: String DEFAULT_AUTHORIZE_RESPONSE_TYPE: String @@ -6833,6 +6862,88 @@ func (ec *executionContext) fieldContext_Env_MICROSOFT_ACTIVE_DIRECTORY_TENANT_I return fc, nil } +func (ec *executionContext) _Env_TWITCH_CLIENT_ID(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Env_TWITCH_CLIENT_ID(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.TwitchClientID, 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) fieldContext_Env_TWITCH_CLIENT_ID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Env", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Env_TWITCH_CLIENT_SECRET(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Env_TWITCH_CLIENT_SECRET(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.TwitchClientSecret, 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) fieldContext_Env_TWITCH_CLIENT_SECRET(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Env", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Env_ORGANIZATION_NAME(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Env_ORGANIZATION_NAME(ctx, field) if err != nil { @@ -7954,6 +8065,50 @@ func (ec *executionContext) fieldContext_Meta_is_microsoft_login_enabled(ctx con return fc, nil } +func (ec *executionContext) _Meta_is_twitch_login_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Meta_is_twitch_login_enabled(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsTwitchLoginEnabled, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Meta_is_twitch_login_enabled(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Meta", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Meta_is_email_verification_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Meta_is_email_verification_enabled(ctx, field) if err != nil { @@ -10484,6 +10639,8 @@ func (ec *executionContext) fieldContext_Query_meta(ctx context.Context, field g return ec.fieldContext_Meta_is_twitter_login_enabled(ctx, field) case "is_microsoft_login_enabled": return ec.fieldContext_Meta_is_microsoft_login_enabled(ctx, field) + case "is_twitch_login_enabled": + return ec.fieldContext_Meta_is_twitch_login_enabled(ctx, field) case "is_email_verification_enabled": return ec.fieldContext_Meta_is_email_verification_enabled(ctx, field) case "is_basic_authentication_enabled": @@ -11208,6 +11365,10 @@ func (ec *executionContext) fieldContext_Query__env(ctx context.Context, field g return ec.fieldContext_Env_MICROSOFT_CLIENT_SECRET(ctx, field) case "MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID": return ec.fieldContext_Env_MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID(ctx, field) + case "TWITCH_CLIENT_ID": + return ec.fieldContext_Env_TWITCH_CLIENT_ID(ctx, field) + case "TWITCH_CLIENT_SECRET": + return ec.fieldContext_Env_TWITCH_CLIENT_SECRET(ctx, field) case "ORGANIZATION_NAME": return ec.fieldContext_Env_ORGANIZATION_NAME(ctx, field) case "ORGANIZATION_LOGO": @@ -17715,7 +17876,7 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob asMap[k] = v } - fieldsInOrder := [...]string{"ACCESS_TOKEN_EXPIRY_TIME", "ADMIN_SECRET", "CUSTOM_ACCESS_TOKEN_SCRIPT", "OLD_ADMIN_SECRET", "SMTP_HOST", "SMTP_PORT", "SMTP_USERNAME", "SMTP_PASSWORD", "SMTP_LOCAL_NAME", "SENDER_EMAIL", "SENDER_NAME", "JWT_TYPE", "JWT_SECRET", "JWT_PRIVATE_KEY", "JWT_PUBLIC_KEY", "ALLOWED_ORIGINS", "APP_URL", "RESET_PASSWORD_URL", "APP_COOKIE_SECURE", "ADMIN_COOKIE_SECURE", "DISABLE_EMAIL_VERIFICATION", "DISABLE_BASIC_AUTHENTICATION", "DISABLE_MAGIC_LINK_LOGIN", "DISABLE_LOGIN_PAGE", "DISABLE_SIGN_UP", "DISABLE_REDIS_FOR_ENV", "DISABLE_STRONG_PASSWORD", "DISABLE_MULTI_FACTOR_AUTHENTICATION", "ENFORCE_MULTI_FACTOR_AUTHENTICATION", "ROLES", "PROTECTED_ROLES", "DEFAULT_ROLES", "JWT_ROLE_CLAIM", "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "FACEBOOK_CLIENT_ID", "FACEBOOK_CLIENT_SECRET", "LINKEDIN_CLIENT_ID", "LINKEDIN_CLIENT_SECRET", "APPLE_CLIENT_ID", "APPLE_CLIENT_SECRET", "TWITTER_CLIENT_ID", "TWITTER_CLIENT_SECRET", "MICROSOFT_CLIENT_ID", "MICROSOFT_CLIENT_SECRET", "MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID", "ORGANIZATION_NAME", "ORGANIZATION_LOGO", "DEFAULT_AUTHORIZE_RESPONSE_TYPE", "DEFAULT_AUTHORIZE_RESPONSE_MODE", "DISABLE_PLAYGROUND", "DISABLE_MAIL_OTP_LOGIN", "DISABLE_TOTP_LOGIN"} + fieldsInOrder := [...]string{"ACCESS_TOKEN_EXPIRY_TIME", "ADMIN_SECRET", "CUSTOM_ACCESS_TOKEN_SCRIPT", "OLD_ADMIN_SECRET", "SMTP_HOST", "SMTP_PORT", "SMTP_USERNAME", "SMTP_PASSWORD", "SMTP_LOCAL_NAME", "SENDER_EMAIL", "SENDER_NAME", "JWT_TYPE", "JWT_SECRET", "JWT_PRIVATE_KEY", "JWT_PUBLIC_KEY", "ALLOWED_ORIGINS", "APP_URL", "RESET_PASSWORD_URL", "APP_COOKIE_SECURE", "ADMIN_COOKIE_SECURE", "DISABLE_EMAIL_VERIFICATION", "DISABLE_BASIC_AUTHENTICATION", "DISABLE_MAGIC_LINK_LOGIN", "DISABLE_LOGIN_PAGE", "DISABLE_SIGN_UP", "DISABLE_REDIS_FOR_ENV", "DISABLE_STRONG_PASSWORD", "DISABLE_MULTI_FACTOR_AUTHENTICATION", "ENFORCE_MULTI_FACTOR_AUTHENTICATION", "ROLES", "PROTECTED_ROLES", "DEFAULT_ROLES", "JWT_ROLE_CLAIM", "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "FACEBOOK_CLIENT_ID", "FACEBOOK_CLIENT_SECRET", "LINKEDIN_CLIENT_ID", "LINKEDIN_CLIENT_SECRET", "APPLE_CLIENT_ID", "APPLE_CLIENT_SECRET", "TWITTER_CLIENT_ID", "TWITTER_CLIENT_SECRET", "MICROSOFT_CLIENT_ID", "MICROSOFT_CLIENT_SECRET", "MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID", "TWITCH_CLIENT_ID", "TWITCH_CLIENT_SECRET", "ORGANIZATION_NAME", "ORGANIZATION_LOGO", "DEFAULT_AUTHORIZE_RESPONSE_TYPE", "DEFAULT_AUTHORIZE_RESPONSE_MODE", "DISABLE_PLAYGROUND", "DISABLE_MAIL_OTP_LOGIN", "DISABLE_TOTP_LOGIN"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -18154,6 +18315,24 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob return it, err } it.MicrosoftActiveDirectoryTenantID = data + case "TWITCH_CLIENT_ID": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("TWITCH_CLIENT_ID")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.TwitchClientID = data + case "TWITCH_CLIENT_SECRET": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("TWITCH_CLIENT_SECRET")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.TwitchClientSecret = data case "ORGANIZATION_NAME": var err error @@ -19136,6 +19315,10 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = ec._Env_MICROSOFT_CLIENT_SECRET(ctx, field, obj) case "MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID": out.Values[i] = ec._Env_MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID(ctx, field, obj) + case "TWITCH_CLIENT_ID": + out.Values[i] = ec._Env_TWITCH_CLIENT_ID(ctx, field, obj) + case "TWITCH_CLIENT_SECRET": + out.Values[i] = ec._Env_TWITCH_CLIENT_SECRET(ctx, field, obj) case "ORGANIZATION_NAME": out.Values[i] = ec._Env_ORGANIZATION_NAME(ctx, field, obj) case "ORGANIZATION_LOGO": @@ -19376,6 +19559,11 @@ func (ec *executionContext) _Meta(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { out.Invalids++ } + case "is_twitch_login_enabled": + out.Values[i] = ec._Meta_is_twitch_login_enabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "is_email_verification_enabled": out.Values[i] = ec._Meta_is_email_verification_enabled(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 17549a7..69ece42 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -119,6 +119,8 @@ type Env struct { MicrosoftClientID *string `json:"MICROSOFT_CLIENT_ID,omitempty"` MicrosoftClientSecret *string `json:"MICROSOFT_CLIENT_SECRET,omitempty"` MicrosoftActiveDirectoryTenantID *string `json:"MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID,omitempty"` + TwitchClientID *string `json:"TWITCH_CLIENT_ID,omitempty"` + TwitchClientSecret *string `json:"TWITCH_CLIENT_SECRET,omitempty"` OrganizationName *string `json:"ORGANIZATION_NAME,omitempty"` OrganizationLogo *string `json:"ORGANIZATION_LOGO,omitempty"` AppCookieSecure bool `json:"APP_COOKIE_SECURE"` @@ -198,6 +200,7 @@ type Meta struct { IsAppleLoginEnabled bool `json:"is_apple_login_enabled"` IsTwitterLoginEnabled bool `json:"is_twitter_login_enabled"` IsMicrosoftLoginEnabled bool `json:"is_microsoft_login_enabled"` + IsTwitchLoginEnabled bool `json:"is_twitch_login_enabled"` IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"` IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"` IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` @@ -383,6 +386,8 @@ type UpdateEnvInput struct { MicrosoftClientID *string `json:"MICROSOFT_CLIENT_ID,omitempty"` MicrosoftClientSecret *string `json:"MICROSOFT_CLIENT_SECRET,omitempty"` MicrosoftActiveDirectoryTenantID *string `json:"MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID,omitempty"` + TwitchClientID *string `json:"TWITCH_CLIENT_ID,omitempty"` + TwitchClientSecret *string `json:"TWITCH_CLIENT_SECRET,omitempty"` OrganizationName *string `json:"ORGANIZATION_NAME,omitempty"` OrganizationLogo *string `json:"ORGANIZATION_LOGO,omitempty"` DefaultAuthorizeResponseType *string `json:"DEFAULT_AUTHORIZE_RESPONSE_TYPE,omitempty"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 3d4efb5..900eb7b 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -22,6 +22,7 @@ type Meta { is_apple_login_enabled: Boolean! is_twitter_login_enabled: Boolean! is_microsoft_login_enabled: Boolean! + is_twitch_login_enabled: Boolean! is_email_verification_enabled: Boolean! is_basic_authentication_enabled: Boolean! is_magic_link_login_enabled: Boolean! @@ -175,6 +176,8 @@ type Env { MICROSOFT_CLIENT_ID: String MICROSOFT_CLIENT_SECRET: String MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID: String + TWITCH_CLIENT_ID: String + TWITCH_CLIENT_SECRET: String ORGANIZATION_NAME: String ORGANIZATION_LOGO: String APP_COOKIE_SECURE: Boolean! @@ -302,6 +305,8 @@ input UpdateEnvInput { MICROSOFT_CLIENT_ID: String MICROSOFT_CLIENT_SECRET: String MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID: String + TWITCH_CLIENT_ID: String + TWITCH_CLIENT_SECRET: String ORGANIZATION_NAME: String ORGANIZATION_LOGO: String DEFAULT_AUTHORIZE_RESPONSE_TYPE: String diff --git a/server/handlers/oauth_callback.go b/server/handlers/oauth_callback.go index 9722a20..5301ca7 100644 --- a/server/handlers/oauth_callback.go +++ b/server/handlers/oauth_callback.go @@ -11,11 +11,13 @@ import ( "strings" "time" + "golang.org/x/oauth2" + "github.com/coreos/go-oidc/v3/oidc" "github.com/gin-gonic/gin" "github.com/google/uuid" + log "github.com/sirupsen/logrus" - "golang.org/x/oauth2" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie" @@ -75,6 +77,8 @@ func OAuthCallbackHandler() gin.HandlerFunc { user, err = processTwitterUserInfo(ctx, oauthCode, sessionState) case constants.AuthRecipeMethodMicrosoft: user, err = processMicrosoftUserInfo(ctx, oauthCode) + case constants.AuthRecipeMethodTwitch: + user, err = processTwitchUserInfo(ctx, oauthCode) default: log.Info("Invalid oauth provider") err = fmt.Errorf(`invalid oauth provider`) @@ -703,3 +707,41 @@ func processMicrosoftUserInfo(ctx context.Context, code string) (*models.User, e return user, nil } + +// process twitch user information +func processTwitchUserInfo(ctx context.Context, code string) (*models.User, error) { + oauth2Token, err := oauth.OAuthProviders.TwitchConfig.Exchange(ctx, code) + if err != nil { + log.Debug("Failed to exchange code for token: ", err) + return nil, fmt.Errorf("invalid twitch exchange code: %s", err.Error()) + } + + // Extract the ID Token from OAuth2 token. + rawIDToken, ok := oauth2Token.Extra("id_token").(string) + if !ok { + log.Debug("Failed to extract ID Token from OAuth2 token") + return nil, fmt.Errorf("unable to extract id_token") + } + + // we need to skip issuer check because for common tenant it will return internal issuer which does not match + verifier := oauth.OIDCProviders.TwitchOIDC.Verifier(&oidc.Config{ + ClientID: oauth.OAuthProviders.TwitchConfig.ClientID, + SkipIssuerCheck: true, + }) + + // Parse and verify ID Token payload. + idToken, err := verifier.Verify(ctx, rawIDToken) + if err != nil { + log.Debug("Failed to verify ID Token: ", err) + return nil, fmt.Errorf("unable to verify id_token: %s", err.Error()) + } + + user := &models.User{} + if err := idToken.Claims(&user); err != nil { + log.Debug("Failed to parse ID Token claims: ", err) + return nil, fmt.Errorf("unable to extract claims") + } + + return user, nil +} + diff --git a/server/handlers/oauth_login.go b/server/handlers/oauth_login.go index 8f8d246..f2740d6 100644 --- a/server/handlers/oauth_login.go +++ b/server/handlers/oauth_login.go @@ -4,10 +4,12 @@ import ( "net/http" "strings" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "golang.org/x/oauth2" + "github.com/gin-gonic/gin" + + log "github.com/sirupsen/logrus" + "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/oauth" @@ -227,6 +229,24 @@ func OAuthLoginHandler() gin.HandlerFunc { oauth.OAuthProviders.MicrosoftConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodMicrosoft url := oauth.OAuthProviders.MicrosoftConfig.AuthCodeURL(oauthStateString) c.Redirect(http.StatusTemporaryRedirect, url) + case constants.AuthRecipeMethodTwitch: + if oauth.OAuthProviders.TwitchConfig == nil { + log.Debug("Twitch OAuth provider is not configured") + isProviderConfigured = false + break + } + err := memorystore.Provider.SetState(oauthStateString, constants.AuthRecipeMethodTwitch) + if err != nil { + log.Debug("Error setting state: ", err) + c.JSON(500, gin.H{ + "error": "internal server error", + }) + return + } + // during the init of OAuthProvider authorizer url might be empty + oauth.OAuthProviders.TwitchConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodTwitch + url := oauth.OAuthProviders.TwitchConfig.AuthCodeURL(oauthStateString) + c.Redirect(http.StatusTemporaryRedirect, url) default: log.Debug("Invalid oauth provider: ", provider) c.JSON(422, gin.H{ diff --git a/server/oauth/oauth.go b/server/oauth/oauth.go index 3f02916..7ad29b1 100644 --- a/server/oauth/oauth.go +++ b/server/oauth/oauth.go @@ -4,13 +4,16 @@ import ( "context" "fmt" - "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" + "google.golang.org/appengine/log" + facebookOAuth2 "golang.org/x/oauth2/facebook" githubOAuth2 "golang.org/x/oauth2/github" linkedInOAuth2 "golang.org/x/oauth2/linkedin" microsoftOAuth2 "golang.org/x/oauth2/microsoft" - "google.golang.org/appengine/log" + twitchOAuth2 "golang.org/x/oauth2/twitch" + + "github.com/coreos/go-oidc/v3/oidc" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/memorystore" @@ -29,12 +32,14 @@ type OAuthProvider struct { AppleConfig *oauth2.Config TwitterConfig *oauth2.Config MicrosoftConfig *oauth2.Config + TwitchConfig *oauth2.Config } // OIDCProviders is a struct that contains reference all the OpenID providers type OIDCProvider struct { GoogleOIDC *oidc.Provider MicrosoftOIDC *oidc.Provider + TwitchOIDC *oidc.Provider } var ( @@ -198,5 +203,31 @@ func InitOAuth() error { } } + twitchClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwitchClientID) + if err != nil { + twitchClientID = "" + } + twitchClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyTwitchClientSecret) + if err != nil { + twitchClientSecret = "" + } + + if twitchClientID != "" && twitchClientSecret != "" { + p, err := oidc.NewProvider(ctx, "https://id.twitch.tv/oauth2") + if err != nil { + log.Debugf(ctx, "Error while creating OIDC provider for Twitch: %v", err) + return err + } + + OIDCProviders.TwitchOIDC = p + OAuthProviders.TwitchConfig = &oauth2.Config{ + ClientID: twitchClientID, + ClientSecret: twitchClientSecret, + RedirectURL: "/oauth_callback/twitch", + Endpoint: twitchOAuth2.Endpoint, + Scopes: []string{oidc.ScopeOpenID}, + } + } + return nil } diff --git a/server/resolvers/env.go b/server/resolvers/env.go index 5eb86bd..85b92bf 100644 --- a/server/resolvers/env.go +++ b/server/resolvers/env.go @@ -164,7 +164,12 @@ func EnvResolver(ctx context.Context) (*model.Env, error) { if val, ok := store[constants.EnvKeyMicrosoftActiveDirectoryTenantID]; ok { res.MicrosoftActiveDirectoryTenantID = refs.NewStringRef(val.(string)) } - + if val, ok := store[constants.EnvKeyTwitchClientID]; ok { + res.TwitchClientID = refs.NewStringRef(val.(string)) + } + if val, ok := store[constants.EnvKeyTwitchClientSecret]; ok { + res.TwitchClientSecret = refs.NewStringRef(val.(string)) + } if val, ok := store[constants.EnvKeyOrganizationName]; ok { res.OrganizationName = refs.NewStringRef(val.(string)) } diff --git a/server/resolvers/update_env.go b/server/resolvers/update_env.go index abd43d2..e7ceb70 100644 --- a/server/resolvers/update_env.go +++ b/server/resolvers/update_env.go @@ -34,6 +34,7 @@ func clearSessionIfRequired(currentData, updatedData map[string]interface{}) { isCurrentLinkedInLoginEnabled := currentData[constants.EnvKeyLinkedInClientID] != nil && currentData[constants.EnvKeyLinkedInClientSecret] != nil && currentData[constants.EnvKeyLinkedInClientID].(string) != "" && currentData[constants.EnvKeyLinkedInClientSecret].(string) != "" isCurrentTwitterLoginEnabled := currentData[constants.EnvKeyTwitterClientID] != nil && currentData[constants.EnvKeyTwitterClientSecret] != nil && currentData[constants.EnvKeyTwitterClientID].(string) != "" && currentData[constants.EnvKeyTwitterClientSecret].(string) != "" isCurrentMicrosoftLoginEnabled := currentData[constants.EnvKeyMicrosoftClientID] != nil && currentData[constants.EnvKeyMicrosoftClientSecret] != nil && currentData[constants.EnvKeyMicrosoftClientID].(string) != "" && currentData[constants.EnvKeyMicrosoftClientSecret].(string) != "" + isCurrentTwitchLoginEnabled := currentData[constants.EnvKeyTwitchClientID] != nil && currentData[constants.EnvKeyTwitchClientSecret] != nil && currentData[constants.EnvKeyTwitchClientID].(string) != "" && currentData[constants.EnvKeyTwitchClientSecret].(string) != "" isUpdatedBasicAuthEnabled := !updatedData[constants.EnvKeyDisableBasicAuthentication].(bool) isUpdatedMobileBasicAuthEnabled := !updatedData[constants.EnvKeyDisableMobileBasicAuthentication].(bool) @@ -45,6 +46,7 @@ func clearSessionIfRequired(currentData, updatedData map[string]interface{}) { isUpdatedLinkedInLoginEnabled := updatedData[constants.EnvKeyLinkedInClientID] != nil && updatedData[constants.EnvKeyLinkedInClientSecret] != nil && updatedData[constants.EnvKeyLinkedInClientID].(string) != "" && updatedData[constants.EnvKeyLinkedInClientSecret].(string) != "" isUpdatedTwitterLoginEnabled := updatedData[constants.EnvKeyTwitterClientID] != nil && updatedData[constants.EnvKeyTwitterClientSecret] != nil && updatedData[constants.EnvKeyTwitterClientID].(string) != "" && updatedData[constants.EnvKeyTwitterClientSecret].(string) != "" isUpdatedMicrosoftLoginEnabled := updatedData[constants.EnvKeyMicrosoftClientID] != nil && updatedData[constants.EnvKeyMicrosoftClientSecret] != nil && updatedData[constants.EnvKeyMicrosoftClientID].(string) != "" && updatedData[constants.EnvKeyMicrosoftClientSecret].(string) != "" + isUpdatedTwitchLoginEnabled := updatedData[constants.EnvKeyTwitchClientID] != nil && updatedData[constants.EnvKeyTwitchClientSecret] != nil && updatedData[constants.EnvKeyTwitchClientID].(string) != "" && updatedData[constants.EnvKeyTwitchClientSecret].(string) != "" if isCurrentBasicAuthEnabled && !isUpdatedBasicAuthEnabled { memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodBasicAuth) @@ -85,6 +87,10 @@ func clearSessionIfRequired(currentData, updatedData map[string]interface{}) { if isCurrentMicrosoftLoginEnabled && !isUpdatedMicrosoftLoginEnabled { memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodMicrosoft) } + + if isCurrentTwitchLoginEnabled && !isUpdatedTwitchLoginEnabled { + memorystore.Provider.DeleteSessionForNamespace(constants.AuthRecipeMethodTwitch) + } } // UpdateEnvResolver is a resolver for update config mutation From d7da81d3089d395f4b5de22996cbbd296ad49c0c Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sat, 2 Dec 2023 12:22:27 +0530 Subject: [PATCH 2/3] fix comment for twitch login --- server/handlers/oauth_callback.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/handlers/oauth_callback.go b/server/handlers/oauth_callback.go index 5301ca7..fdc0466 100644 --- a/server/handlers/oauth_callback.go +++ b/server/handlers/oauth_callback.go @@ -722,8 +722,6 @@ func processTwitchUserInfo(ctx context.Context, code string) (*models.User, erro log.Debug("Failed to extract ID Token from OAuth2 token") return nil, fmt.Errorf("unable to extract id_token") } - - // we need to skip issuer check because for common tenant it will return internal issuer which does not match verifier := oauth.OIDCProviders.TwitchOIDC.Verifier(&oidc.Config{ ClientID: oauth.OAuthProviders.TwitchConfig.ClientID, SkipIssuerCheck: true, @@ -744,4 +742,3 @@ func processTwitchUserInfo(ctx context.Context, code string) (*models.User, erro return user, nil } - From cac67b79158c49e6bf8d36b693d0441135f87e3b Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sun, 3 Dec 2023 09:03:22 +0530 Subject: [PATCH 3/3] feat: add totp UI & recovery code (#429) --- app/package-lock.json | 22 ++++---- app/package.json | 2 +- app/src/pages/login.tsx | 2 +- app/yarn.lock | 24 ++++----- server/authenticators/providers/providers.go | 6 +-- server/authenticators/providers/totp/totp.go | 53 +++++++++++++------- server/graph/generated/generated.go | 10 ++-- server/graph/model/models_gen.go | 2 +- server/graph/schema.graphqls | 2 +- server/resolvers/verify_otp.go | 14 +++++- server/test/totp_login_test.go | 12 ++--- 11 files changed, 87 insertions(+), 62 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index cf0f682..f8d8f63 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^1.1.13", + "@authorizerdev/authorizer-react": "^1.1.15", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", @@ -27,9 +27,9 @@ } }, "node_modules/@authorizerdev/authorizer-js": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.6.tgz", - "integrity": "sha512-9+9phHUMF+AeDM0y+XQvIRDoerOXnQ1vfTfYN6KxWN1apdrkAd9nzS1zUsA2uJSnX3fFZOErn83GjbYYCYF1BA==", + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.17.tgz", + "integrity": "sha512-aF/lu9wZR7TBRaRMAes/hy1q8cZzz5Zo60QLU9Iu09sqnhliHJCp5wSkjsVH+V4ER9i7bmJ2HNABTmOdluxj3A==", "dependencies": { "cross-fetch": "^3.1.5" }, @@ -41,11 +41,11 @@ } }, "node_modules/@authorizerdev/authorizer-react": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.13.tgz", - "integrity": "sha512-LmpzyfR0+nEn+bjUrb/QU9b3kiVoYzMBIvcQ1nV4TNvrvVSqbLPKk+GmoIPkiBEtfy/QSM6XFLkiGNGD9BRP+g==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.15.tgz", + "integrity": "sha512-Y71qC4GUAHL0QCNj5mVv0Jwv1cIg4Y0yXRiOeYV21C1NMleyLRXgw4qzJ/Vk8rmXsxqSHmr8SGrwOLcSKA2oMA==", "dependencies": { - "@authorizerdev/authorizer-js": "^1.2.6" + "@authorizerdev/authorizer-js": "^1.2.17" }, "engines": { "node": ">=10" @@ -607,9 +607,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, diff --git a/app/package.json b/app/package.json index 2406108..5221603 100644 --- a/app/package.json +++ b/app/package.json @@ -12,7 +12,7 @@ "author": "Lakhan Samani", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^1.1.13", + "@authorizerdev/authorizer-react": "^1.1.15", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", diff --git a/app/src/pages/login.tsx b/app/src/pages/login.tsx index 0b713de..3e8e4a1 100644 --- a/app/src/pages/login.tsx +++ b/app/src/pages/login.tsx @@ -37,8 +37,8 @@ export default function Login({ urlProps }: { urlProps: Record }) { {view === VIEW_TYPES.LOGIN && (

Login

-
+
{config.is_basic_authentication_enabled && !config.is_magic_link_login_enabled && ( diff --git a/app/yarn.lock b/app/yarn.lock index 2938142..5e2c385 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2,19 +2,19 @@ # yarn lockfile v1 -"@authorizerdev/authorizer-js@^1.2.6": - version "1.2.6" - resolved "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.6.tgz" - integrity sha512-9+9phHUMF+AeDM0y+XQvIRDoerOXnQ1vfTfYN6KxWN1apdrkAd9nzS1zUsA2uJSnX3fFZOErn83GjbYYCYF1BA== +"@authorizerdev/authorizer-js@^1.2.17": + version "1.2.17" + resolved "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.17.tgz" + integrity sha512-aF/lu9wZR7TBRaRMAes/hy1q8cZzz5Zo60QLU9Iu09sqnhliHJCp5wSkjsVH+V4ER9i7bmJ2HNABTmOdluxj3A== dependencies: cross-fetch "^3.1.5" -"@authorizerdev/authorizer-react@^1.1.13": - version "1.1.13" - resolved "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.13.tgz" - integrity sha512-LmpzyfR0+nEn+bjUrb/QU9b3kiVoYzMBIvcQ1nV4TNvrvVSqbLPKk+GmoIPkiBEtfy/QSM6XFLkiGNGD9BRP+g== +"@authorizerdev/authorizer-react@^1.1.15": + version "1.1.15" + resolved "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.15.tgz" + integrity sha512-Y71qC4GUAHL0QCNj5mVv0Jwv1cIg4Y0yXRiOeYV21C1NMleyLRXgw4qzJ/Vk8rmXsxqSHmr8SGrwOLcSKA2oMA== dependencies: - "@authorizerdev/authorizer-js" "^1.2.6" + "@authorizerdev/authorizer-js" "^1.2.17" "@babel/code-frame@^7.22.13": version "7.22.13" @@ -420,9 +420,9 @@ ms@2.1.2: integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== node-fetch@^2.6.12: - version "2.6.12" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz" - integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== + version "2.7.0" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" diff --git a/server/authenticators/providers/providers.go b/server/authenticators/providers/providers.go index 60c0f79..7f43ef5 100644 --- a/server/authenticators/providers/providers.go +++ b/server/authenticators/providers/providers.go @@ -19,7 +19,7 @@ type Provider interface { // Generate totp: to generate totp, store secret into db and returns base64 of QR code image Generate(ctx context.Context, id string) (*AuthenticatorConfig, error) // Validate totp: user passcode with secret stored in our db - Validate(ctx context.Context, passcode string, id string) (bool, error) - // RecoveryCode totp: gives a recovery code for first time user - RecoveryCode(ctx context.Context, id string) (*string, error) + Validate(ctx context.Context, passcode string, userID string) (bool, error) + // ValidateRecoveryCode totp: allows user to validate using recovery code incase if they lost their device + ValidateRecoveryCode(ctx context.Context, recoveryCode, userID string) (bool, error) } diff --git a/server/authenticators/providers/totp/totp.go b/server/authenticators/providers/totp/totp.go index 1a28f87..b02fe29 100644 --- a/server/authenticators/providers/totp/totp.go +++ b/server/authenticators/providers/totp/totp.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "image/png" "time" @@ -113,24 +114,38 @@ func (p *provider) Validate(ctx context.Context, passcode string, userID string) return status, nil } -// RecoveryCode generates a recovery code for a user's TOTP authentication, if not already verified. -func (p *provider) RecoveryCode(ctx context.Context, id string) (*string, error) { +// ValidateRecoveryCode validates a Time-Based One-Time Password (TOTP) recovery code against the stored TOTP recovery code for a user. +func (p *provider) ValidateRecoveryCode(ctx context.Context, recoveryCode, userID string) (bool, error) { // get totp details - // totpModel, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, id, constants.EnvKeyTOTPAuthenticator) - // if err != nil { - // return nil, fmt.Errorf("error while getting totp details from authenticators") - // } - // //TODO *totpModel.RecoveryCode == "null" used to just verify couchbase recoveryCode value to be nil - // // have to find another way round - // if totpModel.RecoveryCode == nil || *totpModel.RecoveryCode == "null" { - // recoveryCode := utils.GenerateTOTPRecoveryCode() - // totpModel.RecoveryCode = &recoveryCode - - // _, err = db.Provider.UpdateAuthenticator(ctx, totpModel) - // if err != nil { - // return nil, fmt.Errorf("error while updaing authenticator table for totp") - // } - // return &recoveryCode, nil - // } - return nil, nil + totpModel, err := db.Provider.GetAuthenticatorDetailsByUserId(ctx, userID, constants.EnvKeyTOTPAuthenticator) + if err != nil { + return false, err + } + // convert recoveryCodes to map + recoveryCodesMap := map[string]bool{} + err = json.Unmarshal([]byte(refs.StringValue(totpModel.RecoveryCodes)), &recoveryCodesMap) + if err != nil { + return false, err + } + // check if recovery code is valid + if val, ok := recoveryCodesMap[recoveryCode]; !ok { + return false, fmt.Errorf("invalid recovery code") + } else if val { + return false, fmt.Errorf("recovery code already used") + } + // update recovery code map + recoveryCodesMap[recoveryCode] = true + // convert recoveryCodesMap to string + jsonData, err := json.Marshal(recoveryCodesMap) + if err != nil { + return false, err + } + recoveryCodesString := string(jsonData) + totpModel.RecoveryCodes = refs.NewStringRef(recoveryCodesString) + // update recovery code map in db + _, err = db.Provider.UpdateAuthenticator(ctx, totpModel) + if err != nil { + return false, err + } + return true, nil } diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 594b5b9..df7829a 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -2899,7 +2899,7 @@ input VerifyOTPRequest { email: String phone_number: String otp: String! - totp: Boolean + is_totp: Boolean # state is used for authorization code grant flow # it is used to get code for an on-going auth process during login # and use that code for setting ` + "`" + `c_hash` + "`" + ` in id_token @@ -18898,7 +18898,7 @@ func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context, asMap[k] = v } - fieldsInOrder := [...]string{"email", "phone_number", "otp", "totp", "state"} + fieldsInOrder := [...]string{"email", "phone_number", "otp", "is_totp", "state"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -18932,15 +18932,15 @@ func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context, return it, err } it.Otp = data - case "totp": + case "is_totp": var err error - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("totp")) + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("is_totp")) data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) if err != nil { return it, err } - it.Totp = data + it.IsTotp = data case "state": var err error diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 69ece42..06a93ee 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -515,7 +515,7 @@ type VerifyOTPRequest struct { Email *string `json:"email,omitempty"` PhoneNumber *string `json:"phone_number,omitempty"` Otp string `json:"otp"` - Totp *bool `json:"totp,omitempty"` + IsTotp *bool `json:"is_totp,omitempty"` State *string `json:"state,omitempty"` } diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 900eb7b..35e6459 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -573,7 +573,7 @@ input VerifyOTPRequest { email: String phone_number: String otp: String! - totp: Boolean + is_totp: Boolean # state is used for authorization code grant flow # it is used to get code for an on-going auth process during login # and use that code for setting `c_hash` in id_token diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index a0eeb13..e056dee 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -56,7 +56,7 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod return res, err } // Verify OTP based on TOPT or OTP - if refs.BoolValue(params.Totp) { + if refs.BoolValue(params.IsTotp) { status, err := authenticators.Provider.Validate(ctx, params.Otp, user.ID) if err != nil { log.Debug("Failed to validate totp: ", err) @@ -64,7 +64,17 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod } if !status { log.Debug("Failed to verify otp request: Incorrect value") - return res, fmt.Errorf(`invalid otp`) + log.Info("Checking if otp is recovery code") + // Check if otp is recovery code + isValidRecoveryCode, err := authenticators.Provider.ValidateRecoveryCode(ctx, params.Otp, user.ID) + if err != nil { + log.Debug("Failed to validate recovery code: ", err) + return nil, fmt.Errorf("error while validating recovery code") + } + if !isValidRecoveryCode { + log.Debug("Failed to verify otp request: Incorrect value") + return res, fmt.Errorf(`invalid otp`) + } } } else { var otp *models.OTP diff --git a/server/test/totp_login_test.go b/server/test/totp_login_test.go index 44d7c3a..11b992f 100644 --- a/server/test/totp_login_test.go +++ b/server/test/totp_login_test.go @@ -99,9 +99,9 @@ func totpLoginTest(t *testing.T, s TestSetup) { cookie = strings.TrimSuffix(cookie, ";") req.Header.Set("Cookie", cookie) valid, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ - Email: &email, - Totp: refs.NewBoolRef(true), - Otp: code, + Email: &email, + IsTotp: refs.NewBoolRef(true), + Otp: code, }) accessToken := valid.AccessToken assert.NoError(t, err) @@ -147,9 +147,9 @@ func totpLoginTest(t *testing.T, s TestSetup) { cookie = strings.TrimSuffix(cookie, ";") req.Header.Set("Cookie", cookie) valid, err = resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{ - Otp: code, - Email: &email, - Totp: refs.NewBoolRef(true), + Otp: code, + Email: &email, + IsTotp: refs.NewBoolRef(true), }) assert.NoError(t, err) assert.NotNil(t, *valid.AccessToken)