Merge pull request #192 from authorizerdev/feat/apple-login

feat: add apple login
This commit is contained in:
Lakhan Samani 2022-06-16 09:48:02 +05:30 committed by GitHub
commit 88f9a10f21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1918 additions and 1557 deletions

3020
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,29 @@
{ {
"name": "app", "name": "app",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "rm -rf build && NODE_ENV=production node ./esbuild.config.js", "build": "rm -rf build && NODE_ENV=production node ./esbuild.config.js",
"start": "NODE_ENV=development node ./esbuild.config.js" "start": "NODE_ENV=development node ./esbuild.config.js"
}, },
"keywords": [], "keywords": [],
"author": "Lakhan Samani", "author": "Lakhan Samani",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^0.23.0", "@authorizerdev/authorizer-react": "^0.24.0",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17", "esbuild": "^0.12.17",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-is": "^17.0.2", "react-is": "^17.0.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"typescript": "^4.3.5", "typescript": "^4.3.5",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react-router-dom": "^5.1.8", "@types/react-router-dom": "^5.1.8",
"@types/styled-components": "^5.1.11" "@types/styled-components": "^5.1.11"
} }
} }

View File

@ -9,7 +9,13 @@ import {
Divider, Divider,
useMediaQuery, useMediaQuery,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { FaGoogle, FaGithub, FaFacebookF, FaLinkedin } from 'react-icons/fa'; import {
FaGoogle,
FaGithub,
FaFacebookF,
FaLinkedin,
FaApple,
} from 'react-icons/fa';
import { TextInputType, HiddenInputType } from '../../constants'; import { TextInputType, HiddenInputType } from '../../constants';
const OAuthConfig = ({ const OAuthConfig = ({
@ -216,7 +222,45 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility} fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility} setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.LINKEDIN_CLIENT_SECRET} inputType={HiddenInputType.LINKEDIN_CLIENT_SECRET}
placeholder="LinkedIn Secret" placeholder="LinkedIn Client Secret"
/>
</Center>
</Flex>
<Flex direction={isNotSmallerScreen ? 'row' : 'column'}>
<Center
w={isNotSmallerScreen ? '55px' : '35px'}
h="35px"
marginRight="1.5%"
border="1px solid #3b5998"
borderRadius="5px"
>
<FaApple style={{ color: '#3b5998' }} />
</Center>
<Center
w={isNotSmallerScreen ? '70%' : '100%'}
mt={isNotSmallerScreen ? '0' : '3'}
marginRight="1.5%"
>
<InputField
borderRadius={5}
variables={envVariables}
setVariables={setVariables}
inputType={TextInputType.APPLE_CLIENT_ID}
placeholder="Apple Client ID"
/>
</Center>
<Center
w={isNotSmallerScreen ? '70%' : '100%'}
mt={isNotSmallerScreen ? '0' : '3'}
>
<InputField
borderRadius={5}
variables={envVariables}
setVariables={setVariables}
fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.APPLE_CLIENT_SECRET}
placeholder="Apple CLient Secret"
/> />
</Center> </Center>
</Flex> </Flex>

View File

@ -8,6 +8,7 @@ export const TextInputType = {
GITHUB_CLIENT_ID: 'GITHUB_CLIENT_ID', GITHUB_CLIENT_ID: 'GITHUB_CLIENT_ID',
FACEBOOK_CLIENT_ID: 'FACEBOOK_CLIENT_ID', FACEBOOK_CLIENT_ID: 'FACEBOOK_CLIENT_ID',
LINKEDIN_CLIENT_ID: 'LINKEDIN_CLIENT_ID', LINKEDIN_CLIENT_ID: 'LINKEDIN_CLIENT_ID',
APPLE_CLIENT_ID: 'APPLE_CLIENT_ID',
JWT_ROLE_CLAIM: 'JWT_ROLE_CLAIM', JWT_ROLE_CLAIM: 'JWT_ROLE_CLAIM',
REDIS_URL: 'REDIS_URL', REDIS_URL: 'REDIS_URL',
SMTP_HOST: 'SMTP_HOST', SMTP_HOST: 'SMTP_HOST',
@ -33,6 +34,7 @@ export const HiddenInputType = {
GITHUB_CLIENT_SECRET: 'GITHUB_CLIENT_SECRET', GITHUB_CLIENT_SECRET: 'GITHUB_CLIENT_SECRET',
FACEBOOK_CLIENT_SECRET: 'FACEBOOK_CLIENT_SECRET', FACEBOOK_CLIENT_SECRET: 'FACEBOOK_CLIENT_SECRET',
LINKEDIN_CLIENT_SECRET: 'LINKEDIN_CLIENT_SECRET', LINKEDIN_CLIENT_SECRET: 'LINKEDIN_CLIENT_SECRET',
APPLE_CLIENT_SECRET: 'APPLE_CLIENT_SECRET',
JWT_SECRET: 'JWT_SECRET', JWT_SECRET: 'JWT_SECRET',
SMTP_PASSWORD: 'SMTP_PASSWORD', SMTP_PASSWORD: 'SMTP_PASSWORD',
ADMIN_SECRET: 'ADMIN_SECRET', ADMIN_SECRET: 'ADMIN_SECRET',
@ -103,6 +105,8 @@ export interface envVarTypes {
FACEBOOK_CLIENT_SECRET: string; FACEBOOK_CLIENT_SECRET: string;
LINKEDIN_CLIENT_ID: string; LINKEDIN_CLIENT_ID: string;
LINKEDIN_CLIENT_SECRET: string; LINKEDIN_CLIENT_SECRET: string;
APPLE_CLIENT_ID: string;
APPLE_CLIENT_SECRET: string;
ROLES: [string] | []; ROLES: [string] | [];
DEFAULT_ROLES: [string] | []; DEFAULT_ROLES: [string] | [];
PROTECTED_ROLES: [string] | []; PROTECTED_ROLES: [string] | [];

View File

@ -28,6 +28,8 @@ export const EnvVariablesQuery = `
FACEBOOK_CLIENT_SECRET, FACEBOOK_CLIENT_SECRET,
LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_ID,
LINKEDIN_CLIENT_SECRET, LINKEDIN_CLIENT_SECRET,
APPLE_CLIENT_ID,
APPLE_CLIENT_SECRET,
DEFAULT_ROLES, DEFAULT_ROLES,
PROTECTED_ROLES, PROTECTED_ROLES,
ROLES, ROLES,

View File

@ -48,6 +48,8 @@ const Environment = () => {
FACEBOOK_CLIENT_SECRET: '', FACEBOOK_CLIENT_SECRET: '',
LINKEDIN_CLIENT_ID: '', LINKEDIN_CLIENT_ID: '',
LINKEDIN_CLIENT_SECRET: '', LINKEDIN_CLIENT_SECRET: '',
APPLE_CLIENT_ID: '',
APPLE_CLIENT_SECRET: '',
ROLES: [], ROLES: [],
DEFAULT_ROLES: [], DEFAULT_ROLES: [],
PROTECTED_ROLES: [], PROTECTED_ROLES: [],
@ -86,6 +88,7 @@ const Environment = () => {
GITHUB_CLIENT_SECRET: false, GITHUB_CLIENT_SECRET: false,
FACEBOOK_CLIENT_SECRET: false, FACEBOOK_CLIENT_SECRET: false,
LINKEDIN_CLIENT_SECRET: false, LINKEDIN_CLIENT_SECRET: false,
APPLE_CLIENT_SECRET: false,
JWT_SECRET: false, JWT_SECRET: false,
SMTP_PASSWORD: false, SMTP_PASSWORD: false,
ADMIN_SECRET: false, ADMIN_SECRET: false,

6
package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "authorizer",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}

View File

@ -79,6 +79,10 @@ const (
EnvKeyLinkedInClientID = "LINKEDIN_CLIENT_ID" EnvKeyLinkedInClientID = "LINKEDIN_CLIENT_ID"
// EnvKeyLinkedinClientSecret key for env variable LINKEDIN_CLIENT_SECRET // EnvKeyLinkedinClientSecret key for env variable LINKEDIN_CLIENT_SECRET
EnvKeyLinkedInClientSecret = "LINKEDIN_CLIENT_SECRET" EnvKeyLinkedInClientSecret = "LINKEDIN_CLIENT_SECRET"
// EnvKeyAppleClientID key for env variable APPLE_CLIENT_ID
EnvKeyAppleClientID = "APPLE_CLIENT_ID"
// EnvKeyAppleClientSecret key for env variable APPLE_CLIENT_SECRET
EnvKeyAppleClientSecret = "APPLE_CLIENT_SECRET"
// EnvKeyOrganizationName key for env variable ORGANIZATION_NAME // EnvKeyOrganizationName key for env variable ORGANIZATION_NAME
EnvKeyOrganizationName = "ORGANIZATION_NAME" EnvKeyOrganizationName = "ORGANIZATION_NAME"
// EnvKeyOrganizationLogo key for env variable ORGANIZATION_LOGO // EnvKeyOrganizationLogo key for env variable ORGANIZATION_LOGO

View File

@ -13,4 +13,6 @@ const (
SignupMethodFacebook = "facebook" SignupMethodFacebook = "facebook"
// SignupMethodLinkedin is the linkedin signup method // SignupMethodLinkedin is the linkedin signup method
SignupMethodLinkedIn = "linkedin" SignupMethodLinkedIn = "linkedin"
// SignupMethodApple is the apple signup method
SignupMethodApple = "apple"
) )

16
server/env/env.go vendored
View File

@ -70,6 +70,8 @@ func InitAllEnv() error {
osFacebookClientSecret := os.Getenv(constants.EnvKeyFacebookClientSecret) osFacebookClientSecret := os.Getenv(constants.EnvKeyFacebookClientSecret)
osLinkedInClientID := os.Getenv(constants.EnvKeyLinkedInClientID) osLinkedInClientID := os.Getenv(constants.EnvKeyLinkedInClientID)
osLinkedInClientSecret := os.Getenv(constants.EnvKeyLinkedInClientSecret) osLinkedInClientSecret := os.Getenv(constants.EnvKeyLinkedInClientSecret)
osAppleClientID := os.Getenv(constants.EnvKeyAppleClientID)
osAppleClientSecret := os.Getenv(constants.EnvKeyAppleClientSecret)
osResetPasswordURL := os.Getenv(constants.EnvKeyResetPasswordURL) osResetPasswordURL := os.Getenv(constants.EnvKeyResetPasswordURL)
osOrganizationName := os.Getenv(constants.EnvKeyOrganizationName) osOrganizationName := os.Getenv(constants.EnvKeyOrganizationName)
osOrganizationLogo := os.Getenv(constants.EnvKeyOrganizationLogo) osOrganizationLogo := os.Getenv(constants.EnvKeyOrganizationLogo)
@ -361,6 +363,20 @@ func InitAllEnv() error {
envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret
} }
if val, ok := envData[constants.EnvKeyAppleClientID]; !ok || val == "" {
envData[constants.EnvKeyAppleClientID] = osAppleClientID
}
if osFacebookClientID != "" && envData[constants.EnvKeyAppleClientID] != osFacebookClientID {
envData[constants.EnvKeyAppleClientID] = osAppleClientID
}
if val, ok := envData[constants.EnvKeyAppleClientSecret]; !ok || val == "" {
envData[constants.EnvKeyAppleClientSecret] = osAppleClientSecret
}
if osFacebookClientSecret != "" && envData[constants.EnvKeyAppleClientSecret] != osFacebookClientSecret {
envData[constants.EnvKeyAppleClientSecret] = osAppleClientSecret
}
if val, ok := envData[constants.EnvKeyResetPasswordURL]; !ok || val == "" { if val, ok := envData[constants.EnvKeyResetPasswordURL]; !ok || val == "" {
envData[constants.EnvKeyResetPasswordURL] = strings.TrimPrefix(osResetPasswordURL, "/") envData[constants.EnvKeyResetPasswordURL] = strings.TrimPrefix(osResetPasswordURL, "/")
} }

View File

@ -57,6 +57,8 @@ type ComplexityRoot struct {
AdminSecret func(childComplexity int) int AdminSecret func(childComplexity int) int
AllowedOrigins func(childComplexity int) int AllowedOrigins func(childComplexity int) int
AppURL func(childComplexity int) int AppURL func(childComplexity int) int
AppleClientID func(childComplexity int) int
AppleClientSecret func(childComplexity int) int
ClientID func(childComplexity int) int ClientID func(childComplexity int) int
ClientSecret func(childComplexity int) int ClientSecret func(childComplexity int) int
CustomAccessTokenScript func(childComplexity int) int CustomAccessTokenScript func(childComplexity int) int
@ -113,6 +115,7 @@ type ComplexityRoot struct {
Meta struct { Meta struct {
ClientID func(childComplexity int) int ClientID func(childComplexity int) int
IsAppleLoginEnabled func(childComplexity int) int
IsBasicAuthenticationEnabled func(childComplexity int) int IsBasicAuthenticationEnabled func(childComplexity int) int
IsEmailVerificationEnabled func(childComplexity int) int IsEmailVerificationEnabled func(childComplexity int) int
IsFacebookLoginEnabled func(childComplexity int) int IsFacebookLoginEnabled func(childComplexity int) int
@ -335,6 +338,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Env.AppURL(childComplexity), true return e.complexity.Env.AppURL(childComplexity), true
case "Env.APPLE_CLIENT_ID":
if e.complexity.Env.AppleClientID == nil {
break
}
return e.complexity.Env.AppleClientID(childComplexity), true
case "Env.APPLE_CLIENT_SECRET":
if e.complexity.Env.AppleClientSecret == nil {
break
}
return e.complexity.Env.AppleClientSecret(childComplexity), true
case "Env.CLIENT_ID": case "Env.CLIENT_ID":
if e.complexity.Env.ClientID == nil { if e.complexity.Env.ClientID == nil {
break break
@ -664,6 +681,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Meta.ClientID(childComplexity), true return e.complexity.Meta.ClientID(childComplexity), true
case "Meta.is_apple_login_enabled":
if e.complexity.Meta.IsAppleLoginEnabled == nil {
break
}
return e.complexity.Meta.IsAppleLoginEnabled(childComplexity), true
case "Meta.is_basic_authentication_enabled": case "Meta.is_basic_authentication_enabled":
if e.complexity.Meta.IsBasicAuthenticationEnabled == nil { if e.complexity.Meta.IsBasicAuthenticationEnabled == nil {
break break
@ -1377,6 +1401,7 @@ type Meta {
is_facebook_login_enabled: Boolean! is_facebook_login_enabled: Boolean!
is_github_login_enabled: Boolean! is_github_login_enabled: Boolean!
is_linkedin_login_enabled: Boolean! is_linkedin_login_enabled: Boolean!
is_apple_login_enabled: Boolean!
is_email_verification_enabled: Boolean! is_email_verification_enabled: Boolean!
is_basic_authentication_enabled: Boolean! is_basic_authentication_enabled: Boolean!
is_magic_link_login_enabled: Boolean! is_magic_link_login_enabled: Boolean!
@ -1489,6 +1514,8 @@ type Env {
FACEBOOK_CLIENT_SECRET: String FACEBOOK_CLIENT_SECRET: String
LINKEDIN_CLIENT_ID: String LINKEDIN_CLIENT_ID: String
LINKEDIN_CLIENT_SECRET: String LINKEDIN_CLIENT_SECRET: String
APPLE_CLIENT_ID: String
APPLE_CLIENT_SECRET: String
ORGANIZATION_NAME: String ORGANIZATION_NAME: String
ORGANIZATION_LOGO: String ORGANIZATION_LOGO: String
} }
@ -1538,6 +1565,8 @@ input UpdateEnvInput {
FACEBOOK_CLIENT_SECRET: String FACEBOOK_CLIENT_SECRET: String
LINKEDIN_CLIENT_ID: String LINKEDIN_CLIENT_ID: String
LINKEDIN_CLIENT_SECRET: String LINKEDIN_CLIENT_SECRET: String
APPLE_CLIENT_ID: String
APPLE_CLIENT_SECRET: String
ORGANIZATION_NAME: String ORGANIZATION_NAME: String
ORGANIZATION_LOGO: String ORGANIZATION_LOGO: String
} }
@ -3695,6 +3724,70 @@ func (ec *executionContext) _Env_LINKEDIN_CLIENT_SECRET(ctx context.Context, fie
return ec.marshalOString2ᚖstring(ctx, field.Selections, res) return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
} }
func (ec *executionContext) _Env_APPLE_CLIENT_ID(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Env",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.AppleClientID, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _Env_APPLE_CLIENT_SECRET(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Env",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.AppleClientSecret, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _Env_ORGANIZATION_NAME(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { func (ec *executionContext) _Env_ORGANIZATION_NAME(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -4135,6 +4228,41 @@ func (ec *executionContext) _Meta_is_linkedin_login_enabled(ctx context.Context,
return ec.marshalNBoolean2bool(ctx, field.Selections, res) return ec.marshalNBoolean2bool(ctx, field.Selections, res)
} }
func (ec *executionContext) _Meta_is_apple_login_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Meta",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.IsAppleLoginEnabled, 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) _Meta_is_email_verification_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { func (ec *executionContext) _Meta_is_email_verification_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -8707,6 +8835,22 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob
if err != nil { if err != nil {
return it, err return it, err
} }
case "APPLE_CLIENT_ID":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("APPLE_CLIENT_ID"))
it.AppleClientID, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
case "APPLE_CLIENT_SECRET":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("APPLE_CLIENT_SECRET"))
it.AppleClientSecret, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
case "ORGANIZATION_NAME": case "ORGANIZATION_NAME":
var err error var err error
@ -9179,6 +9323,10 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj
out.Values[i] = ec._Env_LINKEDIN_CLIENT_ID(ctx, field, obj) out.Values[i] = ec._Env_LINKEDIN_CLIENT_ID(ctx, field, obj)
case "LINKEDIN_CLIENT_SECRET": case "LINKEDIN_CLIENT_SECRET":
out.Values[i] = ec._Env_LINKEDIN_CLIENT_SECRET(ctx, field, obj) out.Values[i] = ec._Env_LINKEDIN_CLIENT_SECRET(ctx, field, obj)
case "APPLE_CLIENT_ID":
out.Values[i] = ec._Env_APPLE_CLIENT_ID(ctx, field, obj)
case "APPLE_CLIENT_SECRET":
out.Values[i] = ec._Env_APPLE_CLIENT_SECRET(ctx, field, obj)
case "ORGANIZATION_NAME": case "ORGANIZATION_NAME":
out.Values[i] = ec._Env_ORGANIZATION_NAME(ctx, field, obj) out.Values[i] = ec._Env_ORGANIZATION_NAME(ctx, field, obj)
case "ORGANIZATION_LOGO": case "ORGANIZATION_LOGO":
@ -9295,6 +9443,11 @@ func (ec *executionContext) _Meta(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "is_apple_login_enabled":
out.Values[i] = ec._Meta_is_apple_login_enabled(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "is_email_verification_enabled": case "is_email_verification_enabled":
out.Values[i] = ec._Meta_is_email_verification_enabled(ctx, field, obj) out.Values[i] = ec._Meta_is_email_verification_enabled(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {

View File

@ -67,6 +67,8 @@ type Env struct {
FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"` FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"`
LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"` LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"`
LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"`
AppleClientID *string `json:"APPLE_CLIENT_ID"`
AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"` OrganizationName *string `json:"ORGANIZATION_NAME"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"` OrganizationLogo *string `json:"ORGANIZATION_LOGO"`
} }
@ -119,6 +121,7 @@ type Meta struct {
IsFacebookLoginEnabled bool `json:"is_facebook_login_enabled"` IsFacebookLoginEnabled bool `json:"is_facebook_login_enabled"`
IsGithubLoginEnabled bool `json:"is_github_login_enabled"` IsGithubLoginEnabled bool `json:"is_github_login_enabled"`
IsLinkedinLoginEnabled bool `json:"is_linkedin_login_enabled"` IsLinkedinLoginEnabled bool `json:"is_linkedin_login_enabled"`
IsAppleLoginEnabled bool `json:"is_apple_login_enabled"`
IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"` IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"`
IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"` IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"`
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
@ -221,6 +224,8 @@ type UpdateEnvInput struct {
FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"` FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"`
LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"` LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"`
LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"`
AppleClientID *string `json:"APPLE_CLIENT_ID"`
AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"`
OrganizationName *string `json:"ORGANIZATION_NAME"` OrganizationName *string `json:"ORGANIZATION_NAME"`
OrganizationLogo *string `json:"ORGANIZATION_LOGO"` OrganizationLogo *string `json:"ORGANIZATION_LOGO"`
} }

View File

@ -19,6 +19,7 @@ type Meta {
is_facebook_login_enabled: Boolean! is_facebook_login_enabled: Boolean!
is_github_login_enabled: Boolean! is_github_login_enabled: Boolean!
is_linkedin_login_enabled: Boolean! is_linkedin_login_enabled: Boolean!
is_apple_login_enabled: Boolean!
is_email_verification_enabled: Boolean! is_email_verification_enabled: Boolean!
is_basic_authentication_enabled: Boolean! is_basic_authentication_enabled: Boolean!
is_magic_link_login_enabled: Boolean! is_magic_link_login_enabled: Boolean!
@ -131,6 +132,8 @@ type Env {
FACEBOOK_CLIENT_SECRET: String FACEBOOK_CLIENT_SECRET: String
LINKEDIN_CLIENT_ID: String LINKEDIN_CLIENT_ID: String
LINKEDIN_CLIENT_SECRET: String LINKEDIN_CLIENT_SECRET: String
APPLE_CLIENT_ID: String
APPLE_CLIENT_SECRET: String
ORGANIZATION_NAME: String ORGANIZATION_NAME: String
ORGANIZATION_LOGO: String ORGANIZATION_LOGO: String
} }
@ -180,6 +183,8 @@ input UpdateEnvInput {
FACEBOOK_CLIENT_SECRET: String FACEBOOK_CLIENT_SECRET: String
LINKEDIN_CLIENT_ID: String LINKEDIN_CLIENT_ID: String
LINKEDIN_CLIENT_SECRET: String LINKEDIN_CLIENT_SECRET: String
APPLE_CLIENT_ID: String
APPLE_CLIENT_SECRET: String
ORGANIZATION_NAME: String ORGANIZATION_NAME: String
ORGANIZATION_LOGO: String ORGANIZATION_LOGO: String
} }

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -64,6 +65,8 @@ func OAuthCallbackHandler() gin.HandlerFunc {
user, err = processFacebookUserInfo(code) user, err = processFacebookUserInfo(code)
case constants.SignupMethodLinkedIn: case constants.SignupMethodLinkedIn:
user, err = processLinkedInUserInfo(code) user, err = processLinkedInUserInfo(code)
case constants.SignupMethodApple:
user, err = processAppleUserInfo(code)
default: default:
log.Info("Invalid oauth provider") log.Info("Invalid oauth provider")
err = fmt.Errorf(`invalid oauth provider`) err = fmt.Errorf(`invalid oauth provider`)
@ -222,7 +225,7 @@ func OAuthCallbackHandler() gin.HandlerFunc {
redirectURL = redirectURL + "?" + strings.TrimPrefix(params, "&") redirectURL = redirectURL + "?" + strings.TrimPrefix(params, "&")
} }
c.Redirect(http.StatusTemporaryRedirect, redirectURL) c.Redirect(http.StatusFound, redirectURL)
} }
} }
@ -261,7 +264,7 @@ func processGoogleUserInfo(code string) (models.User, error) {
func processGithubUserInfo(code string) (models.User, error) { func processGithubUserInfo(code string) (models.User, error) {
user := models.User{} user := models.User{}
token, err := oauth.OAuthProviders.GithubConfig.Exchange(oauth2.NoContext, code) oauth2Token, err := oauth.OAuthProviders.GithubConfig.Exchange(oauth2.NoContext, code)
if err != nil { if err != nil {
log.Debug("Failed to exchange code for token: ", err) log.Debug("Failed to exchange code for token: ", err)
return user, fmt.Errorf("invalid github exchange code: %s", err.Error()) return user, fmt.Errorf("invalid github exchange code: %s", err.Error())
@ -273,7 +276,7 @@ func processGithubUserInfo(code string) (models.User, error) {
return user, fmt.Errorf("error creating github user info request: %s", err.Error()) return user, fmt.Errorf("error creating github user info request: %s", err.Error())
} }
req.Header = http.Header{ req.Header = http.Header{
"Authorization": []string{fmt.Sprintf("token %s", token.AccessToken)}, "Authorization": []string{fmt.Sprintf("token %s", oauth2Token.AccessToken)},
} }
response, err := client.Do(req) response, err := client.Do(req)
@ -289,8 +292,8 @@ func processGithubUserInfo(code string) (models.User, error) {
return user, fmt.Errorf("failed to read github response body: %s", err.Error()) return user, fmt.Errorf("failed to read github response body: %s", err.Error())
} }
if response.StatusCode >= 400 { if response.StatusCode >= 400 {
log.Debug("Failed to request linkedin user info: ", string(body)) log.Debug("Failed to request github user info: ", string(body))
return user, fmt.Errorf("failed to request linkedin user info: %s", string(body)) return user, fmt.Errorf("failed to request github user info: %s", string(body))
} }
userRawData := make(map[string]string) userRawData := make(map[string]string)
@ -320,13 +323,13 @@ func processGithubUserInfo(code string) (models.User, error) {
func processFacebookUserInfo(code string) (models.User, error) { func processFacebookUserInfo(code string) (models.User, error) {
user := models.User{} user := models.User{}
token, err := oauth.OAuthProviders.FacebookConfig.Exchange(oauth2.NoContext, code) oauth2Token, err := oauth.OAuthProviders.FacebookConfig.Exchange(oauth2.NoContext, code)
if err != nil { if err != nil {
log.Debug("Invalid facebook exchange code: ", err) log.Debug("Invalid facebook exchange code: ", err)
return user, fmt.Errorf("invalid facebook exchange code: %s", err.Error()) return user, fmt.Errorf("invalid facebook exchange code: %s", err.Error())
} }
client := http.Client{} client := http.Client{}
req, err := http.NewRequest("GET", constants.FacebookUserInfoURL+token.AccessToken, nil) req, err := http.NewRequest("GET", constants.FacebookUserInfoURL+oauth2Token.AccessToken, nil)
if err != nil { if err != nil {
log.Debug("Error creating facebook user info request: ", err) log.Debug("Error creating facebook user info request: ", err)
return user, fmt.Errorf("error creating facebook user info request: %s", err.Error()) return user, fmt.Errorf("error creating facebook user info request: %s", err.Error())
@ -345,8 +348,8 @@ func processFacebookUserInfo(code string) (models.User, error) {
return user, fmt.Errorf("failed to read facebook response body: %s", err.Error()) return user, fmt.Errorf("failed to read facebook response body: %s", err.Error())
} }
if response.StatusCode >= 400 { if response.StatusCode >= 400 {
log.Debug("Failed to request linkedin user info: ", string(body)) log.Debug("Failed to request facebook user info: ", string(body))
return user, fmt.Errorf("failed to request linkedin user info: %s", string(body)) return user, fmt.Errorf("failed to request facebook user info: %s", string(body))
} }
userRawData := make(map[string]interface{}) userRawData := make(map[string]interface{})
json.Unmarshal(body, &userRawData) json.Unmarshal(body, &userRawData)
@ -371,7 +374,7 @@ func processFacebookUserInfo(code string) (models.User, error) {
func processLinkedInUserInfo(code string) (models.User, error) { func processLinkedInUserInfo(code string) (models.User, error) {
user := models.User{} user := models.User{}
token, err := oauth.OAuthProviders.LinkedInConfig.Exchange(oauth2.NoContext, code) oauth2Token, err := oauth.OAuthProviders.LinkedInConfig.Exchange(oauth2.NoContext, code)
if err != nil { if err != nil {
log.Debug("Failed to exchange code for token: ", err) log.Debug("Failed to exchange code for token: ", err)
return user, fmt.Errorf("invalid linkedin exchange code: %s", err.Error()) return user, fmt.Errorf("invalid linkedin exchange code: %s", err.Error())
@ -384,7 +387,7 @@ func processLinkedInUserInfo(code string) (models.User, error) {
return user, fmt.Errorf("error creating linkedin user info request: %s", err.Error()) return user, fmt.Errorf("error creating linkedin user info request: %s", err.Error())
} }
req.Header = http.Header{ req.Header = http.Header{
"Authorization": []string{fmt.Sprintf("Bearer %s", token.AccessToken)}, "Authorization": []string{fmt.Sprintf("Bearer %s", oauth2Token.AccessToken)},
} }
response, err := client.Do(req) response, err := client.Do(req)
@ -414,7 +417,7 @@ func processLinkedInUserInfo(code string) (models.User, error) {
return user, fmt.Errorf("error creating linkedin user info request: %s", err.Error()) return user, fmt.Errorf("error creating linkedin user info request: %s", err.Error())
} }
req.Header = http.Header{ req.Header = http.Header{
"Authorization": []string{fmt.Sprintf("Bearer %s", token.AccessToken)}, "Authorization": []string{fmt.Sprintf("Bearer %s", oauth2Token.AccessToken)},
} }
response, err = client.Do(req) response, err = client.Do(req)
@ -450,3 +453,56 @@ func processLinkedInUserInfo(code string) (models.User, error) {
return user, nil return user, nil
} }
func processAppleUserInfo(code string) (models.User, error) {
user := models.User{}
oauth2Token, err := oauth.OAuthProviders.AppleConfig.Exchange(oauth2.NoContext, code)
if err != nil {
log.Debug("Failed to exchange code for token: ", err)
return user, fmt.Errorf("invalid apple 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 user, fmt.Errorf("unable to extract id_token")
}
tokenSplit := strings.Split(rawIDToken, ".")
claimsData := tokenSplit[1]
decodedClaimsData, err := base64.RawURLEncoding.DecodeString(claimsData)
if err != nil {
log.Debugf("Failed to decrypt claims %s: %s", claimsData, err.Error())
return user, fmt.Errorf("failed to decrypt claims data: %s", err.Error())
}
claims := make(map[string]interface{})
err = json.Unmarshal(decodedClaimsData, &claims)
if err != nil {
log.Debug("Failed to unmarshal claims data: ", err)
return user, fmt.Errorf("failed to unmarshal claims data: %s", err.Error())
}
if val, ok := claims["email"]; !ok {
log.Debug("Failed to extract email from claims.")
return user, fmt.Errorf("unable to extract email, please check the scopes enabled for your app. It needs `email`, `name` scopes")
} else {
user.Email = val.(string)
}
if val, ok := claims["name"]; ok {
nameData := val.(map[string]interface{})
if nameVal, ok := nameData["firstName"]; ok {
givenName := nameVal.(string)
user.GivenName = &givenName
}
if nameVal, ok := nameData["lastName"]; ok {
familyName := nameVal.(string)
user.FamilyName = &familyName
}
}
return user, err
}

View File

@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
@ -114,7 +115,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
return return
} }
// during the init of OAuthProvider authorizer url might be empty // during the init of OAuthProvider authorizer url might be empty
oauth.OAuthProviders.GoogleConfig.RedirectURL = hostname + "/oauth_callback/google" oauth.OAuthProviders.GoogleConfig.RedirectURL = hostname + "/oauth_callback/" + constants.SignupMethodGoogle
url := oauth.OAuthProviders.GoogleConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.GoogleConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
case constants.SignupMethodGithub: case constants.SignupMethodGithub:
@ -131,7 +132,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
}) })
return return
} }
oauth.OAuthProviders.GithubConfig.RedirectURL = hostname + "/oauth_callback/github" oauth.OAuthProviders.GithubConfig.RedirectURL = hostname + "/oauth_callback/" + constants.SignupMethodGithub
url := oauth.OAuthProviders.GithubConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.GithubConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
case constants.SignupMethodFacebook: case constants.SignupMethodFacebook:
@ -148,7 +149,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
}) })
return return
} }
oauth.OAuthProviders.FacebookConfig.RedirectURL = hostname + "/oauth_callback/facebook" oauth.OAuthProviders.FacebookConfig.RedirectURL = hostname + "/oauth_callback/" + constants.SignupMethodFacebook
url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
case constants.SignupMethodLinkedIn: case constants.SignupMethodLinkedIn:
@ -165,9 +166,28 @@ func OAuthLoginHandler() gin.HandlerFunc {
}) })
return return
} }
oauth.OAuthProviders.LinkedInConfig.RedirectURL = hostname + "/oauth_callback/linkedin" oauth.OAuthProviders.LinkedInConfig.RedirectURL = hostname + "/oauth_callback/" + constants.SignupMethodLinkedIn
url := oauth.OAuthProviders.LinkedInConfig.AuthCodeURL(oauthStateString) url := oauth.OAuthProviders.LinkedInConfig.AuthCodeURL(oauthStateString)
c.Redirect(http.StatusTemporaryRedirect, url) c.Redirect(http.StatusTemporaryRedirect, url)
case constants.SignupMethodApple:
if oauth.OAuthProviders.AppleConfig == nil {
log.Debug("Apple OAuth provider is not configured")
isProviderConfigured = false
break
}
err := memorystore.Provider.SetState(oauthStateString, constants.SignupMethodApple)
if err != nil {
log.Debug("Error setting state: ", err)
c.JSON(500, gin.H{
"error": "internal server error",
})
return
}
oauth.OAuthProviders.AppleConfig.RedirectURL = hostname + "/oauth_callback/" + constants.SignupMethodApple
// there is scope encoding issue with oauth2 and how apple expects, hence added scope manually
// check: https://github.com/golang/oauth2/issues/449
url := oauth.OAuthProviders.AppleConfig.AuthCodeURL(oauthStateString, oauth2.SetAuthURLParam("response_mode", "form_post")) + "&scope=name email"
c.Redirect(http.StatusTemporaryRedirect, url)
default: default:
log.Debug("Invalid oauth provider: ", provider) log.Debug("Invalid oauth provider: ", provider)
c.JSON(422, gin.H{ c.JSON(422, gin.H{

View File

@ -19,6 +19,7 @@ type OAuthProvider struct {
GithubConfig *oauth2.Config GithubConfig *oauth2.Config
FacebookConfig *oauth2.Config FacebookConfig *oauth2.Config
LinkedInConfig *oauth2.Config LinkedInConfig *oauth2.Config
AppleConfig *oauth2.Config
} }
// OIDCProviders is a struct that contains reference all the OpenID providers // OIDCProviders is a struct that contains reference all the OpenID providers
@ -112,5 +113,25 @@ func InitOAuth() error {
} }
} }
appleClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAppleClientID)
if err != nil {
appleClientID = ""
}
appleClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAppleClientSecret)
if err != nil {
appleClientSecret = ""
}
if appleClientID != "" && appleClientSecret != "" {
OAuthProviders.AppleConfig = &oauth2.Config{
ClientID: appleClientID,
ClientSecret: appleClientSecret,
RedirectURL: "/oauth_callback/apple",
Endpoint: oauth2.Endpoint{
AuthURL: "https://appleid.apple.com/auth/authorize",
TokenURL: "https://appleid.apple.com/auth/token",
},
}
}
return nil return nil
} }

View File

@ -136,6 +136,12 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
if val, ok := store[constants.EnvKeyLinkedInClientSecret]; ok { if val, ok := store[constants.EnvKeyLinkedInClientSecret]; ok {
res.LinkedinClientSecret = utils.NewStringRef(val.(string)) res.LinkedinClientSecret = utils.NewStringRef(val.(string))
} }
if val, ok := store[constants.EnvKeyAppleClientID]; ok {
res.AppleClientID = utils.NewStringRef(val.(string))
}
if val, ok := store[constants.EnvKeyAppleClientSecret]; ok {
res.AppleClientSecret = utils.NewStringRef(val.(string))
}
if val, ok := store[constants.EnvKeyOrganizationName]; ok { if val, ok := store[constants.EnvKeyOrganizationName]; ok {
res.OrganizationName = utils.NewStringRef(val.(string)) res.OrganizationName = utils.NewStringRef(val.(string))
} }

View File

@ -43,16 +43,28 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) {
linkedClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyLinkedInClientID) linkedClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyLinkedInClientID)
if err != nil { if err != nil {
log.Debug("Failed to get Facebook Client ID from environment variable", err) log.Debug("Failed to get LinkedIn Client ID from environment variable", err)
linkedClientID = "" linkedClientID = ""
} }
linkedInClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyLinkedInClientSecret) linkedInClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyLinkedInClientSecret)
if err != nil { if err != nil {
log.Debug("Failed to get Facebook Client Secret from environment variable", err) log.Debug("Failed to get LinkedIn Client Secret from environment variable", err)
linkedInClientSecret = "" linkedInClientSecret = ""
} }
appleClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAppleClientID)
if err != nil {
log.Debug("Failed to get Apple Client ID from environment variable", err)
appleClientID = ""
}
appleClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAppleClientSecret)
if err != nil {
log.Debug("Failed to get Apple Client Secret from environment variable", err)
appleClientSecret = ""
}
githubClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID) githubClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID)
if err != nil { if err != nil {
log.Debug("Failed to get Github Client ID from environment variable", err) log.Debug("Failed to get Github Client ID from environment variable", err)
@ -96,6 +108,7 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) {
IsGithubLoginEnabled: githubClientID != "" && githubClientSecret != "", IsGithubLoginEnabled: githubClientID != "" && githubClientSecret != "",
IsFacebookLoginEnabled: facebookClientID != "" && facebookClientSecret != "", IsFacebookLoginEnabled: facebookClientID != "" && facebookClientSecret != "",
IsLinkedinLoginEnabled: linkedClientID != "" && linkedInClientSecret != "", IsLinkedinLoginEnabled: linkedClientID != "" && linkedInClientSecret != "",
IsAppleLoginEnabled: appleClientID != "" && appleClientSecret != "",
IsBasicAuthenticationEnabled: !isBasicAuthDisabled, IsBasicAuthenticationEnabled: !isBasicAuthDisabled,
IsEmailVerificationEnabled: !isEmailVerificationDisabled, IsEmailVerificationEnabled: !isEmailVerificationDisabled,
IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled, IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled,

View File

@ -23,6 +23,7 @@ func InitRouter(log *logrus.Logger) *gin.Engine {
router.GET("/playground", handlers.PlaygroundHandler()) router.GET("/playground", handlers.PlaygroundHandler())
router.GET("/oauth_login/:oauth_provider", handlers.OAuthLoginHandler()) router.GET("/oauth_login/:oauth_provider", handlers.OAuthLoginHandler())
router.GET("/oauth_callback/:oauth_provider", handlers.OAuthCallbackHandler()) router.GET("/oauth_callback/:oauth_provider", handlers.OAuthCallbackHandler())
router.POST("/oauth_callback/:oauth_provider", handlers.OAuthCallbackHandler())
router.GET("/verify_email", handlers.VerifyEmailHandler()) router.GET("/verify_email", handlers.VerifyEmailHandler())
// OPEN ID routes // OPEN ID routes
router.GET("/.well-known/openid-configuration", handlers.OpenIDConfigurationHandler()) router.GET("/.well-known/openid-configuration", handlers.OpenIDConfigurationHandler())