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/6] 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/6] 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/6] 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) From 32fcba0f8d5731d5b47b3c986822842a0dc676ba Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Sun, 3 Dec 2023 22:27:56 +0530 Subject: [PATCH 4/6] Fix/forgot password (#430) * fix: forgot password shown with magic link login * fix: forgot password shown with magic link login * fix is basic auth enabled --- app/package-lock.json | 25 +++-- app/package.json | 2 +- app/src/pages/login.tsx | 27 +++-- app/yarn.lock | 24 +++-- server/graph/generated/generated.go | 152 +++++++++++++++++++++++++--- server/graph/model/models_gen.go | 34 ++++--- server/graph/schema.graphqls | 2 + server/resolvers/meta.go | 42 +++++--- 8 files changed, 232 insertions(+), 76 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index f8d8f63..62ac19d 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.15", + "@authorizerdev/authorizer-react": "^1.1.18", "@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.17", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.17.tgz", - "integrity": "sha512-aF/lu9wZR7TBRaRMAes/hy1q8cZzz5Zo60QLU9Iu09sqnhliHJCp5wSkjsVH+V4ER9i7bmJ2HNABTmOdluxj3A==", + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.18.tgz", + "integrity": "sha512-9j5U/4lqaaEcG78Zli+TtLJ0migSKhFwnXXunulAGTZOzQSTCJ/CSSPip5wWNa/Mkr6gdEMwk1HYfhIdk2A9Mg==", "dependencies": { "cross-fetch": "^3.1.5" }, @@ -41,11 +41,12 @@ } }, "node_modules/@authorizerdev/authorizer-react": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.15.tgz", - "integrity": "sha512-Y71qC4GUAHL0QCNj5mVv0Jwv1cIg4Y0yXRiOeYV21C1NMleyLRXgw4qzJ/Vk8rmXsxqSHmr8SGrwOLcSKA2oMA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.18.tgz", + "integrity": "sha512-5SgFzG1VatmrMpl9XKwPcoVmCayA4Hn+sd2I9CwRlCWkdcna4pGJL8kYesuIGjGagS9394qp4ICRLRZ35wXj8A==", "dependencies": { - "@authorizerdev/authorizer-js": "^1.2.17" + "@authorizerdev/authorizer-js": "^1.2.18", + "validator": "^13.11.0" }, "engines": { "node": ">=10" @@ -847,6 +848,14 @@ "node": ">=4.2.0" } }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", diff --git a/app/package.json b/app/package.json index 5221603..530b65c 100644 --- a/app/package.json +++ b/app/package.json @@ -12,7 +12,7 @@ "author": "Lakhan Samani", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "^1.1.15", + "@authorizerdev/authorizer-react": "^1.1.18", "@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 3e8e4a1..d53d36c 100644 --- a/app/src/pages/login.tsx +++ b/app/src/pages/login.tsx @@ -32,6 +32,7 @@ const FooterContent = styled.div` export default function Login({ urlProps }: { urlProps: Record }) { const { config } = useAuthorizer(); const [view, setView] = useState(VIEW_TYPES.LOGIN); + const isBasicAuth = config.is_basic_authentication_enabled; return ( {view === VIEW_TYPES.LOGIN && ( @@ -39,22 +40,26 @@ export default function Login({ urlProps }: { urlProps: Record }) {

Login


- {config.is_basic_authentication_enabled && + {(config.is_basic_authentication_enabled || + config.is_mobile_basic_authentication_enabled) && !config.is_magic_link_login_enabled && ( )} {config.is_magic_link_login_enabled && ( )} -
- setView(VIEW_TYPES.FORGOT_PASSWORD)} - style={{ marginBottom: 10 }} - > - Forgot Password? - -
+ {(config.is_basic_authentication_enabled || + config.is_mobile_basic_authentication_enabled) && ( +
+ setView(VIEW_TYPES.FORGOT_PASSWORD)} + style={{ marginBottom: 10 }} + > + Forgot Password? + +
+ )}
)} {view === VIEW_TYPES.FORGOT_PASSWORD && ( @@ -81,7 +86,7 @@ export default function Login({ urlProps }: { urlProps: Record }) { !config.is_magic_link_login_enabled && config.is_sign_up_enabled && ( - Don't have an account? Sign Up + Don't have an account?   Sign Up )}
diff --git a/app/yarn.lock b/app/yarn.lock index 5e2c385..d9fa871 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2,19 +2,20 @@ # yarn lockfile v1 -"@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== +"@authorizerdev/authorizer-js@^1.2.18": + version "1.2.18" + resolved "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-1.2.18.tgz" + integrity sha512-9j5U/4lqaaEcG78Zli+TtLJ0migSKhFwnXXunulAGTZOzQSTCJ/CSSPip5wWNa/Mkr6gdEMwk1HYfhIdk2A9Mg== dependencies: cross-fetch "^3.1.5" -"@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== +"@authorizerdev/authorizer-react@^1.1.18": + version "1.1.18" + resolved "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-1.1.18.tgz" + integrity sha512-5SgFzG1VatmrMpl9XKwPcoVmCayA4Hn+sd2I9CwRlCWkdcna4pGJL8kYesuIGjGagS9394qp4ICRLRZ35wXj8A== dependencies: - "@authorizerdev/authorizer-js" "^1.2.17" + "@authorizerdev/authorizer-js" "^1.2.18" + validator "^13.11.0" "@babel/code-frame@^7.22.13": version "7.22.13" @@ -594,6 +595,11 @@ typescript@^4.3.5: resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz" integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +validator@^13.11.0: + version "13.11.0" + resolved "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz" + integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ== + value-equal@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz" diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index df7829a..b0792bc 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -162,22 +162,24 @@ type ComplexityRoot struct { } Meta struct { - ClientID func(childComplexity int) int - IsAppleLoginEnabled func(childComplexity int) int - IsBasicAuthenticationEnabled func(childComplexity int) int - IsEmailVerificationEnabled func(childComplexity int) int - IsFacebookLoginEnabled func(childComplexity int) int - IsGithubLoginEnabled func(childComplexity int) int - IsGoogleLoginEnabled func(childComplexity int) int - IsLinkedinLoginEnabled func(childComplexity int) int - IsMagicLinkLoginEnabled func(childComplexity int) int - IsMicrosoftLoginEnabled func(childComplexity int) int - 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 + ClientID func(childComplexity int) int + IsAppleLoginEnabled func(childComplexity int) int + IsBasicAuthenticationEnabled func(childComplexity int) int + IsEmailVerificationEnabled func(childComplexity int) int + IsFacebookLoginEnabled func(childComplexity int) int + IsGithubLoginEnabled func(childComplexity int) int + IsGoogleLoginEnabled func(childComplexity int) int + IsLinkedinLoginEnabled func(childComplexity int) int + IsMagicLinkLoginEnabled func(childComplexity int) int + IsMicrosoftLoginEnabled func(childComplexity int) int + IsMobileBasicAuthenticationEnabled func(childComplexity int) int + IsMultiFactorAuthEnabled func(childComplexity int) int + IsPhoneVerificationEnabled 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 } Mutation struct { @@ -1142,6 +1144,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Meta.IsMicrosoftLoginEnabled(childComplexity), true + case "Meta.is_mobile_basic_authentication_enabled": + if e.complexity.Meta.IsMobileBasicAuthenticationEnabled == nil { + break + } + + return e.complexity.Meta.IsMobileBasicAuthenticationEnabled(childComplexity), true + case "Meta.is_multi_factor_auth_enabled": if e.complexity.Meta.IsMultiFactorAuthEnabled == nil { break @@ -1149,6 +1158,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Meta.IsMultiFactorAuthEnabled(childComplexity), true + case "Meta.is_phone_verification_enabled": + if e.complexity.Meta.IsPhoneVerificationEnabled == nil { + break + } + + return e.complexity.Meta.IsPhoneVerificationEnabled(childComplexity), true + case "Meta.is_sign_up_enabled": if e.complexity.Meta.IsSignUpEnabled == nil { break @@ -2355,6 +2371,8 @@ type Meta { is_sign_up_enabled: Boolean! is_strong_password_enabled: Boolean! is_multi_factor_auth_enabled: Boolean! + is_mobile_basic_authentication_enabled: Boolean! + is_phone_verification_enabled: Boolean! } type User { @@ -8373,6 +8391,94 @@ func (ec *executionContext) fieldContext_Meta_is_multi_factor_auth_enabled(ctx c return fc, nil } +func (ec *executionContext) _Meta_is_mobile_basic_authentication_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Meta_is_mobile_basic_authentication_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.IsMobileBasicAuthenticationEnabled, 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_mobile_basic_authentication_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_phone_verification_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Meta_is_phone_verification_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.IsPhoneVerificationEnabled, 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_phone_verification_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) _Mutation_signup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_signup(ctx, field) if err != nil { @@ -10653,6 +10759,10 @@ func (ec *executionContext) fieldContext_Query_meta(ctx context.Context, field g return ec.fieldContext_Meta_is_strong_password_enabled(ctx, field) case "is_multi_factor_auth_enabled": return ec.fieldContext_Meta_is_multi_factor_auth_enabled(ctx, field) + case "is_mobile_basic_authentication_enabled": + return ec.fieldContext_Meta_is_mobile_basic_authentication_enabled(ctx, field) + case "is_phone_verification_enabled": + return ec.fieldContext_Meta_is_phone_verification_enabled(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Meta", field.Name) }, @@ -19594,6 +19704,16 @@ func (ec *executionContext) _Meta(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { out.Invalids++ } + case "is_mobile_basic_authentication_enabled": + out.Values[i] = ec._Meta_is_mobile_basic_authentication_enabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "is_phone_verification_enabled": + out.Values[i] = ec._Meta_is_phone_verification_enabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 06a93ee..45f5889 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -191,22 +191,24 @@ type MagicLinkLoginInput struct { } type Meta struct { - Version string `json:"version"` - ClientID string `json:"client_id"` - IsGoogleLoginEnabled bool `json:"is_google_login_enabled"` - IsFacebookLoginEnabled bool `json:"is_facebook_login_enabled"` - IsGithubLoginEnabled bool `json:"is_github_login_enabled"` - IsLinkedinLoginEnabled bool `json:"is_linkedin_login_enabled"` - 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"` - IsSignUpEnabled bool `json:"is_sign_up_enabled"` - IsStrongPasswordEnabled bool `json:"is_strong_password_enabled"` - IsMultiFactorAuthEnabled bool `json:"is_multi_factor_auth_enabled"` + Version string `json:"version"` + ClientID string `json:"client_id"` + IsGoogleLoginEnabled bool `json:"is_google_login_enabled"` + IsFacebookLoginEnabled bool `json:"is_facebook_login_enabled"` + IsGithubLoginEnabled bool `json:"is_github_login_enabled"` + IsLinkedinLoginEnabled bool `json:"is_linkedin_login_enabled"` + 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"` + IsSignUpEnabled bool `json:"is_sign_up_enabled"` + IsStrongPasswordEnabled bool `json:"is_strong_password_enabled"` + IsMultiFactorAuthEnabled bool `json:"is_multi_factor_auth_enabled"` + IsMobileBasicAuthenticationEnabled bool `json:"is_mobile_basic_authentication_enabled"` + IsPhoneVerificationEnabled bool `json:"is_phone_verification_enabled"` } type MobileLoginInput struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 35e6459..07d3678 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -29,6 +29,8 @@ type Meta { is_sign_up_enabled: Boolean! is_strong_password_enabled: Boolean! is_multi_factor_auth_enabled: Boolean! + is_mobile_basic_authentication_enabled: Boolean! + is_phone_verification_enabled: Boolean! } type User { diff --git a/server/resolvers/meta.go b/server/resolvers/meta.go index 9290a41..cbab1e0 100644 --- a/server/resolvers/meta.go +++ b/server/resolvers/meta.go @@ -106,6 +106,16 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) { log.Debug("Failed to get Disable Basic Authentication from environment variable", err) isBasicAuthDisabled = true } + isMobileBasicAuthDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMobileBasicAuthentication) + if err != nil { + log.Debug("Failed to get Disable Basic Authentication from environment variable", err) + isMobileBasicAuthDisabled = true + } + isMobileVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisablePhoneVerification) + if err != nil { + log.Debug("Failed to get Disable Basic Authentication from environment variable", err) + isMobileVerificationDisabled = true + } isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) if err != nil { @@ -138,21 +148,23 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) { } metaInfo := model.Meta{ - Version: constants.VERSION, - ClientID: clientID, - IsGoogleLoginEnabled: googleClientID != "" && googleClientSecret != "", - IsGithubLoginEnabled: githubClientID != "" && githubClientSecret != "", - IsFacebookLoginEnabled: facebookClientID != "" && facebookClientSecret != "", - IsLinkedinLoginEnabled: linkedClientID != "" && linkedInClientSecret != "", - IsAppleLoginEnabled: appleClientID != "" && appleClientSecret != "", - IsTwitterLoginEnabled: twitterClientID != "" && twitterClientSecret != "", - IsMicrosoftLoginEnabled: microsoftClientID != "" && microsoftClientSecret != "", - IsBasicAuthenticationEnabled: !isBasicAuthDisabled, - IsEmailVerificationEnabled: !isEmailVerificationDisabled, - IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled, - IsSignUpEnabled: !isSignUpDisabled, - IsStrongPasswordEnabled: !isStrongPasswordDisabled, - IsMultiFactorAuthEnabled: !isMultiFactorAuthenticationEnabled, + Version: constants.VERSION, + ClientID: clientID, + IsGoogleLoginEnabled: googleClientID != "" && googleClientSecret != "", + IsGithubLoginEnabled: githubClientID != "" && githubClientSecret != "", + IsFacebookLoginEnabled: facebookClientID != "" && facebookClientSecret != "", + IsLinkedinLoginEnabled: linkedClientID != "" && linkedInClientSecret != "", + IsAppleLoginEnabled: appleClientID != "" && appleClientSecret != "", + IsTwitterLoginEnabled: twitterClientID != "" && twitterClientSecret != "", + IsMicrosoftLoginEnabled: microsoftClientID != "" && microsoftClientSecret != "", + IsBasicAuthenticationEnabled: !isBasicAuthDisabled, + IsEmailVerificationEnabled: !isEmailVerificationDisabled, + IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled, + IsSignUpEnabled: !isSignUpDisabled, + IsStrongPasswordEnabled: !isStrongPasswordDisabled, + IsMultiFactorAuthEnabled: !isMultiFactorAuthenticationEnabled, + IsMobileBasicAuthenticationEnabled: !isMobileBasicAuthDisabled, + IsPhoneVerificationEnabled: !isMobileVerificationDisabled, } return &metaInfo, nil } From 3fa892431e980d28d274da093e4a9876ef574e67 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Mon, 4 Dec 2023 09:26:59 +0530 Subject: [PATCH 5/6] fix: phone_number_verified_at set during signup --- server/resolvers/signup.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index 8eb3368..29167c7 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -222,12 +222,12 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR log.Debug("Error getting email verification disabled: ", err) isEmailVerificationDisabled = true } - if isEmailVerificationDisabled { + if isEmailVerificationDisabled && isEmailSignup { now := time.Now().Unix() user.EmailVerifiedAt = &now } disablePhoneVerification, _ := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisablePhoneVerification) - if disablePhoneVerification { + if disablePhoneVerification && isMobileSignup { now := time.Now().Unix() user.PhoneNumberVerifiedAt = &now } From e8b99f73c38daa0d9190c2b40f8ccd68687fae4b Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Mon, 4 Dec 2023 11:28:27 +0530 Subject: [PATCH 6/6] fix: phone number verification --- server/handlers/verify_email.go | 8 ++++- server/resolvers/login.go | 2 +- server/resolvers/signup.go | 2 +- server/resolvers/verify_otp.go | 56 ++++++++++++++++++++++--------- server/test/mobile_signup_test.go | 7 +++- server/test/verify_email_test.go | 6 +++- 6 files changed, 61 insertions(+), 20 deletions(-) diff --git a/server/handlers/verify_email.go b/server/handlers/verify_email.go index 47f61d3..34f9d9f 100644 --- a/server/handlers/verify_email.go +++ b/server/handlers/verify_email.go @@ -74,7 +74,13 @@ func VerifyEmailHandler() gin.HandlerFunc { now := time.Now().Unix() user.EmailVerifiedAt = &now isSignUp = true - db.Provider.UpdateUser(c, user) + user, err = db.Provider.UpdateUser(c, user) + if err != nil { + log.Debug("Error updating user: ", err) + errorRes["error"] = err.Error() + utils.HandleRedirectORJsonResponse(c, http.StatusBadRequest, errorRes, generateRedirectURL(redirectURL, errorRes)) + return + } } // delete from verification table db.Provider.DeleteVerificationRequest(c, verificationRequest) diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 708e3b4..34edbca 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -78,7 +78,7 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes } if err != nil { log.Debug("Failed to get user: ", err) - return res, fmt.Errorf(`bad user credentials`) + return res, fmt.Errorf(`user not found`) } if user.RevokedTimestamp != nil { log.Debug("User access is revoked") diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index 29167c7..f947f2c 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -73,7 +73,7 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR } isEmailSignup := email != "" isMobileSignup := phoneNumber != "" - if isBasicAuthDisabled { + if isBasicAuthDisabled && isEmailSignup { log.Debug("Basic authentication is disabled") return res, fmt.Errorf(`basic authentication is disabled for this instance`) } diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go index e056dee..16a10c7 100644 --- a/server/resolvers/verify_otp.go +++ b/server/resolvers/verify_otp.go @@ -36,24 +36,29 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod return res, fmt.Errorf(`invalid session: %s`, err.Error()) } - if refs.StringValue(params.Email) == "" && refs.StringValue(params.PhoneNumber) == "" { + email := strings.TrimSpace(refs.StringValue(params.Email)) + phoneNumber := strings.TrimSpace(refs.StringValue(params.PhoneNumber)) + if email == "" && phoneNumber == "" { log.Debug("Email or phone number is required") - return res, fmt.Errorf(`email or phone_number is required`) - } - currentField := models.FieldNameEmail - if refs.StringValue(params.Email) == "" { - currentField = models.FieldNamePhoneNumber + return res, fmt.Errorf(`email or phone number is required`) } + isEmailVerification := email != "" + isMobileVerification := phoneNumber != "" // Get user by email or phone number var user *models.User - if currentField == models.FieldNameEmail { + if isEmailVerification { user, err = db.Provider.GetUserByEmail(ctx, refs.StringValue(params.Email)) + if err != nil { + log.Debug("Failed to get user by email: ", err) + } } else { user, err = db.Provider.GetUserByPhoneNumber(ctx, refs.StringValue(params.PhoneNumber)) + if err != nil { + log.Debug("Failed to get user by phone number: ", err) + } } if user == nil || err != nil { - log.Debug("Failed to get user by email or phone number: ", err) - return res, err + return res, fmt.Errorf(`user not found`) } // Verify OTP based on TOPT or OTP if refs.BoolValue(params.IsTotp) { @@ -78,14 +83,19 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod } } else { var otp *models.OTP - if currentField == models.FieldNameEmail { + if isEmailVerification { otp, err = db.Provider.GetOTPByEmail(ctx, refs.StringValue(params.Email)) + if err != nil { + log.Debug(`Failed to get otp request for email: `, err.Error()) + } } else { otp, err = db.Provider.GetOTPByPhoneNumber(ctx, refs.StringValue(params.PhoneNumber)) + if err != nil { + log.Debug(`Failed to get otp request for phone number: `, err.Error()) + } } if otp == nil && err != nil { - log.Debugf("Failed to get otp request for %s: %s", currentField, err.Error()) - return res, fmt.Errorf(`invalid %s: %s`, currentField, err.Error()) + return res, fmt.Errorf(`OTP not found`) } if params.Otp != otp.Otp { log.Debug("Failed to verify otp request: Incorrect value") @@ -104,10 +114,26 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod return res, fmt.Errorf(`invalid session: %s`, err.Error()) } - isSignUp := user.EmailVerifiedAt == nil && user.PhoneNumberVerifiedAt == nil - // TODO - Add Login method in DB when we introduce OTP for social media login + isSignUp := false + if user.EmailVerifiedAt == nil && isEmailVerification { + isSignUp = true + now := time.Now().Unix() + user.EmailVerifiedAt = &now + } + if user.PhoneNumberVerifiedAt == nil && isMobileVerification { + isSignUp = true + now := time.Now().Unix() + user.PhoneNumberVerifiedAt = &now + } + if isSignUp { + user, err = db.Provider.UpdateUser(ctx, user) + if err != nil { + log.Debug("Failed to update user: ", err) + return res, err + } + } loginMethod := constants.AuthRecipeMethodBasicAuth - if currentField == models.FieldNamePhoneNumber { + if isMobileVerification { loginMethod = constants.AuthRecipeMethodMobileOTP } roles := strings.Split(user.Roles, ",") diff --git a/server/test/mobile_signup_test.go b/server/test/mobile_signup_test.go index 949895b..7bfba1c 100644 --- a/server/test/mobile_signup_test.go +++ b/server/test/mobile_signup_test.go @@ -98,12 +98,17 @@ func mobileSingupTest(t *testing.T, s TestSetup) { }) assert.Nil(t, err) assert.NotEmpty(t, otpRes.Message) + // Check if phone number is verified + user, err = db.Provider.GetUserByPhoneNumber(ctx, phoneNumber) + assert.NoError(t, err) + assert.NotNil(t, user) + assert.NotNil(t, user.PhoneNumberVerifiedAt) res, err = resolvers.SignupResolver(ctx, model.SignUpInput{ PhoneNumber: refs.NewStringRef(phoneNumber), Password: s.TestInfo.Password, ConfirmPassword: s.TestInfo.Password, }) - assert.Error(t, err) + assert.Error(t, err, "should throw duplicate error") assert.Nil(t, res) cleanData("1234567890@authorizer.dev") }) diff --git a/server/test/verify_email_test.go b/server/test/verify_email_test.go index 491f349..19416c1 100644 --- a/server/test/verify_email_test.go +++ b/server/test/verify_email_test.go @@ -35,7 +35,11 @@ func verifyEmailTest(t *testing.T, s TestSetup) { }) assert.Nil(t, err) assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty") - + // Check if phone number is verified + user1, err := db.Provider.GetUserByEmail(ctx, email) + assert.NoError(t, err) + assert.NotNil(t, user1) + assert.NotNil(t, user1.EmailVerifiedAt) cleanData(email) }) }