Merge pull request #206 from authorizerdev/feat/2fa

feat: add mutifactor authentication
This commit is contained in:
Lakhan Samani 2022-08-07 11:11:56 +05:30 committed by GitHub
commit 0714b4360b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 2300 additions and 322 deletions

View File

@ -11,14 +11,26 @@ clean:
rm -rf build rm -rf build
test: test:
rm -rf server/test/test.db && rm -rf test.db && cd server && go clean --testcache && TEST_DBS="sqlite" go test -p 1 -v ./test rm -rf server/test/test.db && rm -rf test.db && cd server && go clean --testcache && TEST_DBS="sqlite" go test -p 1 -v ./test
test-mongodb:
docker run -d --name authorizer_mongodb_db -p 27017:27017 mongo:4.4.15
cd server && go clean --testcache && TEST_DBS="mongodb" go test -p 1 -v ./test
docker rm -vf authorizer_mongodb_db
test-scylladb:
docker run -d --name authorizer_scylla_db -p 9042:9042 scylladb/scylla
cd server && go clean --testcache && TEST_DBS="scylladb" go test -p 1 -v ./test
docker rm -vf authorizer_scylla_db
test-arangodb:
docker run -d --name authorizer_arangodb -p 8529:8529 -e ARANGO_NO_AUTH=1 arangodb/arangodb:3.8.4
cd server && go clean --testcache && TEST_DBS="arangodb" go test -p 1 -v ./test
docker rm -vf authorizer_arangodb
test-all-db: test-all-db:
rm -rf server/test/test.db && rm -rf test.db rm -rf server/test/test.db && rm -rf test.db
docker run -d --name authorizer_scylla_db -p 9042:9042 scylladb/scylla docker run -d --name authorizer_scylla_db -p 9042:9042 scylladb/scylla
docker run -d --name authorizer_mongodb_db -p 27017:27017 mongo:4.4.15 docker run -d --name authorizer_mongodb_db -p 27017:27017 mongo:4.4.15
docker run -d --name authorizer_arangodb -p 8529:8529 -e ARANGO_NO_AUTH=1 arangodb/arangodb:3.8.4 docker run -d --name authorizer_arangodb -p 8529:8529 -e ARANGO_NO_AUTH=1 arangodb/arangodb:3.8.4
cd server && go clean --testcache && TEST_DBS="sqlite,mongodb,arangodb,scylladb" go test -p 1 -v ./test cd server && go clean --testcache && TEST_DBS="sqlite,mongodb,arangodb,scylladb" go test -p 1 -v ./test
docker rm -vf authorizer_mongodb_db
docker rm -vf authorizer_scylla_db docker rm -vf authorizer_scylla_db
docker rm -vf authorizer_mongodb_db
docker rm -vf authorizer_arangodb docker rm -vf authorizer_arangodb
generate: generate:
cd server && go get github.com/99designs/gqlgen/cmd@v0.14.0 && go run github.com/99designs/gqlgen generate cd server && go get github.com/99designs/gqlgen/cmd@v0.14.0 && go run github.com/99designs/gqlgen generate

30
app/package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^0.25.0", "@authorizerdev/authorizer-react": "^0.26.0-beta.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",
@ -26,9 +26,9 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-js": { "node_modules/@authorizerdev/authorizer-js": {
"version": "0.14.0", "version": "0.17.0-beta.1",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.14.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.17.0-beta.1.tgz",
"integrity": "sha512-cpeeFrmG623QPLn+nf+ACHayZYqW8xokIidGikeboBDJtuAAQB50a54/7HwLHriG2FB7WvPuHQ/9LFFX//N1lg==", "integrity": "sha512-jUlFUrs4Ys6LZ5hclPeRt84teygi+bA57d/IpV9GAqOrfifv70jkFeDln4+Bs0mZk74el23Xn+DR9380mqE4Cg==",
"dependencies": { "dependencies": {
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
}, },
@ -37,11 +37,11 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-react": { "node_modules/@authorizerdev/authorizer-react": {
"version": "0.25.0", "version": "0.26.0-beta.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.25.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.26.0-beta.0.tgz",
"integrity": "sha512-Dt2rZf+cGCVb8dqcJ/9l8Trx+QeXnTdfhER6r/cq0iOnFC9MqWzQPB3RgrlUoMLHtZvKNDXIk1HvfD5hSX9lhw==", "integrity": "sha512-YfyiGYBmbsp3tLWIxOrOZ/hUTCmdMXVE9SLE8m1xsFsxzJJlUhepp0AMahSbH5EyLj5bchOhOw/rzgpnDZDvMw==",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": "^0.14.0", "@authorizerdev/authorizer-js": "^0.17.0-beta.1",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"
@ -852,19 +852,19 @@
}, },
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": { "@authorizerdev/authorizer-js": {
"version": "0.14.0", "version": "0.17.0-beta.1",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.14.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.17.0-beta.1.tgz",
"integrity": "sha512-cpeeFrmG623QPLn+nf+ACHayZYqW8xokIidGikeboBDJtuAAQB50a54/7HwLHriG2FB7WvPuHQ/9LFFX//N1lg==", "integrity": "sha512-jUlFUrs4Ys6LZ5hclPeRt84teygi+bA57d/IpV9GAqOrfifv70jkFeDln4+Bs0mZk74el23Xn+DR9380mqE4Cg==",
"requires": { "requires": {
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
} }
}, },
"@authorizerdev/authorizer-react": { "@authorizerdev/authorizer-react": {
"version": "0.25.0", "version": "0.26.0-beta.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.25.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.26.0-beta.0.tgz",
"integrity": "sha512-Dt2rZf+cGCVb8dqcJ/9l8Trx+QeXnTdfhER6r/cq0iOnFC9MqWzQPB3RgrlUoMLHtZvKNDXIk1HvfD5hSX9lhw==", "integrity": "sha512-YfyiGYBmbsp3tLWIxOrOZ/hUTCmdMXVE9SLE8m1xsFsxzJJlUhepp0AMahSbH5EyLj5bchOhOw/rzgpnDZDvMw==",
"requires": { "requires": {
"@authorizerdev/authorizer-js": "^0.14.0", "@authorizerdev/authorizer-js": "^0.17.0-beta.1",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"

View File

@ -11,7 +11,7 @@
"author": "Lakhan Samani", "author": "Lakhan Samani",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^0.25.0", "@authorizerdev/authorizer-react": "^0.26.0-beta.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",

View File

@ -108,7 +108,7 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility} fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility} setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.GOOGLE_CLIENT_SECRET} inputType={HiddenInputType.GOOGLE_CLIENT_SECRET}
placeholder="Google Secret" placeholder="Google Client Secret"
/> />
</Center> </Center>
</Flex> </Flex>
@ -146,7 +146,7 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility} fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility} setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.GITHUB_CLIENT_SECRET} inputType={HiddenInputType.GITHUB_CLIENT_SECRET}
placeholder="Github Secret" placeholder="Github Client Secret"
/> />
</Center> </Center>
</Flex> </Flex>
@ -184,7 +184,7 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility} fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility} setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.FACEBOOK_CLIENT_SECRET} inputType={HiddenInputType.FACEBOOK_CLIENT_SECRET}
placeholder="Facebook Secret" placeholder="Facebook Client Secret"
/> />
</Center> </Center>
</Flex> </Flex>
@ -260,7 +260,7 @@ const OAuthConfig = ({
fieldVisibility={fieldVisibility} fieldVisibility={fieldVisibility}
setFieldVisibility={setFieldVisibility} setFieldVisibility={setFieldVisibility}
inputType={HiddenInputType.APPLE_CLIENT_SECRET} inputType={HiddenInputType.APPLE_CLIENT_SECRET}
placeholder="Apple CLient Secret" placeholder="Apple Client Secret"
/> />
</Center> </Center>
</Flex> </Flex>

View File

@ -89,6 +89,7 @@ export const UserDetailsQuery = `
roles roles
created_at created_at
revoked_timestamp revoked_timestamp
is_multi_factor_auth_enabled
} }
} }
} }

View File

@ -68,6 +68,7 @@ interface userDataTypes {
roles: [string]; roles: [string];
created_at: number; created_at: number;
revoked_timestamp: number; revoked_timestamp: number;
is_multi_factor_auth_enabled?: boolean;
} }
const enum updateAccessActions { const enum updateAccessActions {
@ -250,6 +251,34 @@ export default function Users() {
break; break;
} }
}; };
const multiFactorAuthUpdateHandler = async (user: userDataTypes) => {
const res = await client
.mutation(UpdateUser, {
params: {
id: user.id,
is_multi_factor_auth_enabled: !user.is_multi_factor_auth_enabled,
},
})
.toPromise();
if (res.data?._update_user?.id) {
toast({
title: `Multi factor authentication ${
user.is_multi_factor_auth_enabled ? 'disabled' : 'enabled'
} for user`,
isClosable: true,
status: 'success',
position: 'bottom-right',
});
updateUserList();
return;
}
toast({
title: 'Multi factor authentication update failed for user',
isClosable: true,
status: 'error',
position: 'bottom-right',
});
};
return ( return (
<Box m="5" py="5" px="10" bg="white" rounded="md"> <Box m="5" py="5" px="10" bg="white" rounded="md">
@ -273,6 +302,7 @@ export default function Users() {
<Th>Roles</Th> <Th>Roles</Th>
<Th>Verified</Th> <Th>Verified</Th>
<Th>Access</Th> <Th>Access</Th>
<Th>MFA</Th>
<Th>Actions</Th> <Th>Actions</Th>
</Tr> </Tr>
</Thead> </Thead>
@ -305,6 +335,19 @@ export default function Users() {
{user.revoked_timestamp ? 'Revoked' : 'Enabled'} {user.revoked_timestamp ? 'Revoked' : 'Enabled'}
</Tag> </Tag>
</Td> </Td>
<Td>
<Tag
size="sm"
variant="outline"
colorScheme={
user.is_multi_factor_auth_enabled ? 'green' : 'red'
}
>
{user.is_multi_factor_auth_enabled
? 'Enabled'
: 'Disabled'}
</Tag>
</Td>
<Td> <Td>
<Menu> <Menu>
<MenuButton as={Button} variant="unstyled" size="sm"> <MenuButton as={Button} variant="unstyled" size="sm">
@ -357,6 +400,19 @@ export default function Users() {
Revoke Access Revoke Access
</MenuItem> </MenuItem>
)} )}
{user.is_multi_factor_auth_enabled ? (
<MenuItem
onClick={() => multiFactorAuthUpdateHandler(user)}
>
Disable MFA
</MenuItem>
) : (
<MenuItem
onClick={() => multiFactorAuthUpdateHandler(user)}
>
Enable MFA
</MenuItem>
)}
</MenuList> </MenuList>
</Menu> </Menu>
</Td> </Td>

View File

@ -47,6 +47,8 @@ const (
EnvKeySmtpPassword = "SMTP_PASSWORD" EnvKeySmtpPassword = "SMTP_PASSWORD"
// EnvKeySenderEmail key for env variable SENDER_EMAIL // EnvKeySenderEmail key for env variable SENDER_EMAIL
EnvKeySenderEmail = "SENDER_EMAIL" EnvKeySenderEmail = "SENDER_EMAIL"
// EnvKeyIsEmailServiceEnabled key for env variable IS_EMAIL_SERVICE_ENABLED
EnvKeyIsEmailServiceEnabled = "IS_EMAIL_SERVICE_ENABLED"
// EnvKeyJwtType key for env variable JWT_TYPE // EnvKeyJwtType key for env variable JWT_TYPE
EnvKeyJwtType = "JWT_TYPE" EnvKeyJwtType = "JWT_TYPE"
// EnvKeyJwtSecret key for env variable JWT_SECRET // EnvKeyJwtSecret key for env variable JWT_SECRET
@ -117,6 +119,12 @@ const (
EnvKeyDisableRedisForEnv = "DISABLE_REDIS_FOR_ENV" EnvKeyDisableRedisForEnv = "DISABLE_REDIS_FOR_ENV"
// EnvKeyDisableStrongPassword key for env variable DISABLE_STRONG_PASSWORD // EnvKeyDisableStrongPassword key for env variable DISABLE_STRONG_PASSWORD
EnvKeyDisableStrongPassword = "DISABLE_STRONG_PASSWORD" EnvKeyDisableStrongPassword = "DISABLE_STRONG_PASSWORD"
// EnvKeyEnforceMultiFactorAuthentication is key for env variable ENFORCE_MULTI_FACTOR_AUTHENTICATION
// If enforced and changed later on, existing user will have MFA but new user will not have MFA
EnvKeyEnforceMultiFactorAuthentication = "ENFORCE_MULTI_FACTOR_AUTHENTICATION"
// EnvKeyDisableMultiFactorAuthentication is key for env variable DISABLE_MULTI_FACTOR_AUTHENTICATION
// this variable is used to completely disable multi factor authentication. It will have no effect on profile preference
EnvKeyDisableMultiFactorAuthentication = "DISABLE_MULTI_FACTOR_AUTHENTICATION"
// Slice variables // Slice variables
// EnvKeyRoles key for env variable ROLES // EnvKeyRoles key for env variable ROLES

View File

@ -9,6 +9,7 @@ type CollectionList struct {
Webhook string Webhook string
WebhookLog string WebhookLog string
EmailTemplate string EmailTemplate string
OTP string
} }
var ( var (
@ -23,5 +24,6 @@ var (
Webhook: Prefix + "webhooks", Webhook: Prefix + "webhooks",
WebhookLog: Prefix + "webhook_logs", WebhookLog: Prefix + "webhook_logs",
EmailTemplate: Prefix + "email_templates", EmailTemplate: Prefix + "email_templates",
OTP: Prefix + "otps",
} }
) )

12
server/db/models/otp.go Normal file
View File

@ -0,0 +1,12 @@
package models
// OTP model for database
type OTP struct {
Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"`
Email string `gorm:"unique" json:"email" bson:"email" cql:"email"`
Otp string `json:"otp" bson:"otp" cql:"otp"`
ExpiresAt int64 `json:"expires_at" bson:"expires_at" cql:"expires_at"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
}

View File

@ -29,6 +29,7 @@ type User struct {
Picture *string `gorm:"type:text" json:"picture" bson:"picture" cql:"picture"` Picture *string `gorm:"type:text" json:"picture" bson:"picture" cql:"picture"`
Roles string `json:"roles" bson:"roles" cql:"roles"` Roles string `json:"roles" bson:"roles" cql:"roles"`
RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp" cql:"revoked_timestamp"` RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp" cql:"revoked_timestamp"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled" bson:"is_multi_factor_auth_enabled" cql:"is_multi_factor_auth_enabled"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"` CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
} }
@ -37,12 +38,12 @@ func (user *User) AsAPIUser() *model.User {
isEmailVerified := user.EmailVerifiedAt != nil isEmailVerified := user.EmailVerifiedAt != nil
isPhoneVerified := user.PhoneNumberVerifiedAt != nil isPhoneVerified := user.PhoneNumberVerifiedAt != nil
id := user.ID // id := user.ID
if strings.Contains(id, Collections.WebhookLog+"/") { // if strings.Contains(id, Collections.User+"/") {
id = strings.TrimPrefix(id, Collections.WebhookLog+"/") // id = strings.TrimPrefix(id, Collections.User+"/")
} // }
return &model.User{ return &model.User{
ID: id, ID: user.ID,
Email: user.Email, Email: user.Email,
EmailVerified: isEmailVerified, EmailVerified: isEmailVerified,
SignupMethods: user.SignupMethods, SignupMethods: user.SignupMethods,
@ -58,6 +59,7 @@ func (user *User) AsAPIUser() *model.User {
Picture: user.Picture, Picture: user.Picture,
Roles: strings.Split(user.Roles, ","), Roles: strings.Split(user.Roles, ","),
RevokedTimestamp: user.RevokedTimestamp, RevokedTimestamp: user.RevokedTimestamp,
IsMultiFactorAuthEnabled: user.IsMultiFactorAuthEnabled,
CreatedAt: refs.NewInt64Ref(user.CreatedAt), CreatedAt: refs.NewInt64Ref(user.CreatedAt),
UpdatedAt: refs.NewInt64Ref(user.UpdatedAt), UpdatedAt: refs.NewInt64Ref(user.UpdatedAt),
} }

View File

@ -25,8 +25,8 @@ type VerificationRequest struct {
func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest { func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest {
id := v.ID id := v.ID
if strings.Contains(id, Collections.WebhookLog+"/") { if strings.Contains(id, Collections.VerificationRequest+"/") {
id = strings.TrimPrefix(id, Collections.WebhookLog+"/") id = strings.TrimPrefix(id, Collections.VerificationRequest+"/")
} }
return &model.VerificationRequest{ return &model.VerificationRequest{

View File

@ -16,6 +16,7 @@ import (
func (p *provider) AddEmailTemplate(ctx context.Context, emailTemplate models.EmailTemplate) (*model.EmailTemplate, error) { func (p *provider) AddEmailTemplate(ctx context.Context, emailTemplate models.EmailTemplate) (*model.EmailTemplate, error) {
if emailTemplate.ID == "" { if emailTemplate.ID == "" {
emailTemplate.ID = uuid.New().String() emailTemplate.ID = uuid.New().String()
emailTemplate.Key = emailTemplate.ID
} }
emailTemplate.Key = emailTemplate.ID emailTemplate.Key = emailTemplate.ID

View File

@ -15,6 +15,7 @@ import (
func (p *provider) AddEnv(ctx context.Context, env models.Env) (models.Env, error) { func (p *provider) AddEnv(ctx context.Context, env models.Env) (models.Env, error) {
if env.ID == "" { if env.ID == "" {
env.ID = uuid.New().String() env.ID = uuid.New().String()
env.Key = env.ID
} }
env.CreatedAt = time.Now().Unix() env.CreatedAt = time.Now().Unix()

View File

@ -0,0 +1,92 @@
package arangodb
import (
"context"
"fmt"
"time"
"github.com/arangodb/go-driver"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/google/uuid"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) {
otp, _ := p.GetOTPByEmail(ctx, otpParam.Email)
shouldCreate := false
if otp == nil {
id := uuid.NewString()
otp = &models.OTP{
ID: id,
Key: id,
Otp: otpParam.Otp,
Email: otpParam.Email,
ExpiresAt: otpParam.ExpiresAt,
CreatedAt: time.Now().Unix(),
}
shouldCreate = true
} else {
otp.Otp = otpParam.Otp
otp.ExpiresAt = otpParam.ExpiresAt
}
otp.UpdatedAt = time.Now().Unix()
otpCollection, _ := p.db.Collection(ctx, models.Collections.OTP)
var meta driver.DocumentMeta
var err error
if shouldCreate {
meta, err = otpCollection.CreateDocument(ctx, otp)
} else {
meta, err = otpCollection.UpdateDocument(ctx, otp.Key, otp)
}
if err != nil {
return nil, err
}
otp.Key = meta.Key
otp.ID = meta.ID.String()
return otp, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
var otp models.OTP
query := fmt.Sprintf("FOR d in %s FILTER d.email == @email RETURN d", models.Collections.OTP)
bindVars := map[string]interface{}{
"email": emailAddress,
}
cursor, err := p.db.Query(ctx, query, bindVars)
if err != nil {
return nil, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if otp.Key == "" {
return nil, fmt.Errorf("email template not found")
}
break
}
_, err := cursor.ReadDocument(ctx, &otp)
if err != nil {
return nil, err
}
}
return &otp, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
otpCollection, _ := p.db.Collection(ctx, models.Collections.OTP)
_, err := otpCollection.RemoveDocument(ctx, otp.ID)
if err != nil {
return err
}
return nil
}

View File

@ -148,6 +148,20 @@ func NewProvider() (*provider, error) {
Sparse: true, Sparse: true,
}) })
otpCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.OTP)
if !otpCollectionExists {
_, err = arangodb.CreateCollection(ctx, models.Collections.OTP, nil)
if err != nil {
return nil, err
}
}
otpCollection, _ := arangodb.Collection(nil, models.Collections.OTP)
otpCollection.EnsureHashIndex(ctx, []string{"email"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
return &provider{ return &provider{
db: arangodb, db: arangodb,
}, err }, err

View File

@ -12,6 +12,7 @@ import (
func (p *provider) AddSession(ctx context.Context, session models.Session) error { func (p *provider) AddSession(ctx context.Context, session models.Session) error {
if session.ID == "" { if session.ID == "" {
session.ID = uuid.New().String() session.ID = uuid.New().String()
session.Key = session.ID
} }
session.CreatedAt = time.Now().Unix() session.CreatedAt = time.Now().Unix()

View File

@ -2,22 +2,26 @@ package arangodb
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/arangodb/go-driver" "github.com/arangodb/go-driver"
arangoDriver "github.com/arangodb/go-driver" arangoDriver "github.com/arangodb/go-driver"
"github.com/google/uuid"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/google/uuid"
) )
// AddUser to save user information in database // AddUser to save user information in database
func (p *provider) AddUser(ctx context.Context, user models.User) (models.User, error) { func (p *provider) AddUser(ctx context.Context, user models.User) (models.User, error) {
if user.ID == "" { if user.ID == "" {
user.ID = uuid.New().String() user.ID = uuid.New().String()
user.Key = user.ID
} }
if user.Roles == "" { if user.Roles == "" {
@ -65,7 +69,7 @@ func (p *provider) DeleteUser(ctx context.Context, user models.User) error {
query := fmt.Sprintf(`FOR d IN %s FILTER d.user_id == @user_id REMOVE { _key: d._key } IN %s`, models.Collections.Session, models.Collections.Session) query := fmt.Sprintf(`FOR d IN %s FILTER d.user_id == @user_id REMOVE { _key: d._key } IN %s`, models.Collections.Session, models.Collections.Session)
bindVars := map[string]interface{}{ bindVars := map[string]interface{}{
"user_id": user.ID, "user_id": user.Key,
} }
cursor, err := p.db.Query(ctx, query, bindVars) cursor, err := p.db.Query(ctx, query, bindVars)
if err != nil { if err != nil {
@ -174,3 +178,36 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
return user, nil return user, nil
} }
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
userInfoBytes, err := json.Marshal(data)
if err != nil {
return err
}
query := ""
if ids != nil && len(ids) > 0 {
keysArray := ""
for _, id := range ids {
keysArray += fmt.Sprintf("'%s', ", id)
}
keysArray = strings.Trim(keysArray, " ")
keysArray = strings.TrimSuffix(keysArray, ",")
query = fmt.Sprintf("FOR u IN %s FILTER u._id IN [%s] UPDATE u._key with %s IN %s", models.Collections.User, keysArray, string(userInfoBytes), models.Collections.User)
} else {
query = fmt.Sprintf("FOR u IN %s UPDATE u._key with %s IN %s", models.Collections.User, string(userInfoBytes), models.Collections.User)
}
_, err = p.db.Query(ctx, query, nil)
if err != nil {
return err
}
return nil
}

View File

@ -15,6 +15,7 @@ import (
func (p *provider) AddVerificationRequest(ctx context.Context, verificationRequest models.VerificationRequest) (models.VerificationRequest, error) { func (p *provider) AddVerificationRequest(ctx context.Context, verificationRequest models.VerificationRequest) (models.VerificationRequest, error) {
if verificationRequest.ID == "" { if verificationRequest.ID == "" {
verificationRequest.ID = uuid.New().String() verificationRequest.ID = uuid.New().String()
verificationRequest.Key = verificationRequest.ID
} }
verificationRequest.CreatedAt = time.Now().Unix() verificationRequest.CreatedAt = time.Now().Unix()

View File

@ -16,6 +16,7 @@ import (
func (p *provider) AddWebhook(ctx context.Context, webhook models.Webhook) (*model.Webhook, error) { func (p *provider) AddWebhook(ctx context.Context, webhook models.Webhook) (*model.Webhook, error) {
if webhook.ID == "" { if webhook.ID == "" {
webhook.ID = uuid.New().String() webhook.ID = uuid.New().String()
webhook.Key = webhook.ID
} }
webhook.Key = webhook.ID webhook.Key = webhook.ID

View File

@ -16,6 +16,7 @@ import (
func (p *provider) AddWebhookLog(ctx context.Context, webhookLog models.WebhookLog) (*model.WebhookLog, error) { func (p *provider) AddWebhookLog(ctx context.Context, webhookLog models.WebhookLog) (*model.WebhookLog, error) {
if webhookLog.ID == "" { if webhookLog.ID == "" {
webhookLog.ID = uuid.New().String() webhookLog.ID = uuid.New().String()
webhookLog.Key = webhookLog.ID
} }
webhookLog.Key = webhookLog.ID webhookLog.Key = webhookLog.ID

View File

@ -0,0 +1,67 @@
package cassandradb
import (
"context"
"fmt"
"time"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/gocql/gocql"
"github.com/google/uuid"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) {
otp, _ := p.GetOTPByEmail(ctx, otpParam.Email)
shouldCreate := false
if otp == nil {
shouldCreate = true
otp = &models.OTP{
ID: uuid.NewString(),
Otp: otpParam.Otp,
Email: otpParam.Email,
ExpiresAt: otpParam.ExpiresAt,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
}
} else {
otp.Otp = otpParam.Otp
otp.ExpiresAt = otpParam.ExpiresAt
}
otp.UpdatedAt = time.Now().Unix()
query := ""
if shouldCreate {
query = fmt.Sprintf(`INSERT INTO %s (id, email, otp, expires_at, created_at, updated_at) VALUES ('%s', '%s', '%s', %d, %d, %d)`, KeySpace+"."+models.Collections.OTP, otp.ID, otp.Email, otp.Otp, otp.ExpiresAt, otp.CreatedAt, otp.UpdatedAt)
} else {
query = fmt.Sprintf(`UPDATE %s SET otp = '%s', expires_at = %d, updated_at = %d WHERE id = '%s'`, KeySpace+"."+models.Collections.OTP, otp.Otp, otp.ExpiresAt, otp.UpdatedAt, otp.ID)
}
err := p.db.Query(query).Exec()
if err != nil {
return nil, err
}
return otp, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
var otp models.OTP
query := fmt.Sprintf(`SELECT id, email, otp, expires_at, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING`, KeySpace+"."+models.Collections.OTP, emailAddress)
err := p.db.Query(query).Consistency(gocql.One).Scan(&otp.ID, &otp.Email, &otp.Otp, &otp.ExpiresAt, &otp.CreatedAt, &otp.UpdatedAt)
if err != nil {
return nil, err
}
return &otp, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
query := fmt.Sprintf("DELETE FROM %s WHERE id = '%s'", KeySpace+"."+models.Collections.OTP, otp.ID)
err := p.db.Query(query).Exec()
if err != nil {
return err
}
return nil
}

View File

@ -13,6 +13,7 @@ import (
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/gocql/gocql" "github.com/gocql/gocql"
cansandraDriver "github.com/gocql/gocql" cansandraDriver "github.com/gocql/gocql"
log "github.com/sirupsen/logrus"
) )
type provider struct { type provider struct {
@ -99,6 +100,7 @@ func NewProvider() (*provider, error) {
cassandraClient.Consistency = gocql.LocalQuorum cassandraClient.Consistency = gocql.LocalQuorum
cassandraClient.ConnectTimeout = 10 * time.Second cassandraClient.ConnectTimeout = 10 * time.Second
cassandraClient.ProtoVersion = 4 cassandraClient.ProtoVersion = 4
cassandraClient.Timeout = 30 * time.Minute // for large data
session, err := cassandraClient.CreateSession() session, err := cassandraClient.CreateSession()
if err != nil { if err != nil {
@ -159,6 +161,13 @@ func NewProvider() (*provider, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// add is_multi_factor_auth_enabled on users table
userTableAlterQuery := fmt.Sprintf(`ALTER TABLE %s.%s ADD is_multi_factor_auth_enabled boolean`, KeySpace, models.Collections.User)
err = session.Query(userTableAlterQuery).Exec()
if err != nil {
log.Debug("Failed to alter table as column exists: ", err)
// return nil, err
}
// token is reserved keyword in cassandra, hence we need to use jwt_token // token is reserved keyword in cassandra, hence we need to use jwt_token
verificationRequestCollectionQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, jwt_token text, identifier text, expires_at bigint, email text, nonce text, redirect_uri text, created_at bigint, updated_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.VerificationRequest) verificationRequestCollectionQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, jwt_token text, identifier text, expires_at bigint, email text, nonce text, redirect_uri text, created_at bigint, updated_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.VerificationRequest)
@ -221,6 +230,17 @@ func NewProvider() (*provider, error) {
return nil, err return nil, err
} }
otpCollection := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, email text, otp text, expires_at bigint, updated_at bigint, created_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.OTP)
err = session.Query(otpCollection).Exec()
if err != nil {
return nil, err
}
otpIndexQuery := fmt.Sprintf("CREATE INDEX IF NOT EXISTS authorizer_otp_email ON %s.%s (email)", KeySpace, models.Collections.OTP)
err = session.Query(otpIndexQuery).Exec()
if err != nil {
return nil, err
}
return &provider{ return &provider{
db: session, db: session,
}, err }, err

View File

@ -107,7 +107,7 @@ func (p *provider) UpdateUser(ctx context.Context, user models.User) (models.Use
} }
if value == nil { if value == nil {
updateFields += fmt.Sprintf("%s = null,", key) updateFields += fmt.Sprintf("%s = null, ", key)
continue continue
} }
@ -122,7 +122,6 @@ func (p *provider) UpdateUser(ctx context.Context, user models.User) (models.Use
updateFields = strings.TrimSuffix(updateFields, ",") updateFields = strings.TrimSuffix(updateFields, ",")
query := fmt.Sprintf("UPDATE %s SET %s WHERE id = '%s'", KeySpace+"."+models.Collections.User, updateFields, user.ID) query := fmt.Sprintf("UPDATE %s SET %s WHERE id = '%s'", KeySpace+"."+models.Collections.User, updateFields, user.ID)
err = p.db.Query(query).Exec() err = p.db.Query(query).Exec()
if err != nil { if err != nil {
return user, err return user, err
@ -173,14 +172,14 @@ func (p *provider) ListUsers(ctx context.Context, pagination model.Pagination) (
// there is no offset in cassandra // there is no offset in cassandra
// so we fetch till limit + offset // so we fetch till limit + offset
// and return the results from offset to limit // and return the results from offset to limit
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.User, pagination.Limit+pagination.Offset) query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.User, pagination.Limit+pagination.Offset)
scanner := p.db.Query(query).Iter().Scanner() scanner := p.db.Query(query).Iter().Scanner()
counter := int64(0) counter := int64(0)
for scanner.Next() { for scanner.Next() {
if counter >= pagination.Offset { if counter >= pagination.Offset {
var user models.User var user models.User
err := scanner.Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt) err := scanner.Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -197,8 +196,8 @@ func (p *provider) ListUsers(ctx context.Context, pagination model.Pagination) (
// GetUserByEmail to get user information from database using email address // GetUserByEmail to get user information from database using email address
func (p *provider) GetUserByEmail(ctx context.Context, email string) (models.User, error) { func (p *provider) GetUserByEmail(ctx context.Context, email string) (models.User, error) {
var user models.User var user models.User
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING", KeySpace+"."+models.Collections.User, email) query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING", KeySpace+"."+models.Collections.User, email)
err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt) err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
if err != nil { if err != nil {
return user, err return user, err
} }
@ -208,10 +207,95 @@ func (p *provider) GetUserByEmail(ctx context.Context, email string) (models.Use
// GetUserByID to get user information from database using user ID // GetUserByID to get user information from database using user ID
func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, error) { func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, error) {
var user models.User var user models.User
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1", KeySpace+"."+models.Collections.User, id) query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1", KeySpace+"."+models.Collections.User, id)
err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt) err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
if err != nil { if err != nil {
return user, err return user, err
} }
return user, nil return user, nil
} }
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
updateFields := ""
for key, value := range data {
if key == "_id" {
continue
}
if key == "_key" {
continue
}
if value == nil {
updateFields += fmt.Sprintf("%s = null,", key)
continue
}
valueType := reflect.TypeOf(value)
if valueType.Name() == "string" {
updateFields += fmt.Sprintf("%s = '%s', ", key, value.(string))
} else {
updateFields += fmt.Sprintf("%s = %v, ", key, value)
}
}
updateFields = strings.Trim(updateFields, " ")
updateFields = strings.TrimSuffix(updateFields, ",")
query := ""
if ids != nil && len(ids) > 0 {
idsString := ""
for _, id := range ids {
idsString += fmt.Sprintf("'%s', ", id)
}
idsString = strings.Trim(idsString, " ")
idsString = strings.TrimSuffix(idsString, ",")
query = fmt.Sprintf("UPDATE %s SET %s WHERE id IN (%s)", KeySpace+"."+models.Collections.User, updateFields, idsString)
err := p.db.Query(query).Exec()
if err != nil {
return err
}
} else {
// get all ids
getUserIDsQuery := fmt.Sprintf(`SELECT id FROM %s`, KeySpace+"."+models.Collections.User)
scanner := p.db.Query(getUserIDsQuery).Iter().Scanner()
// only 100 ids are allowed in 1 query
// hence we need create multiple update queries
idsString := ""
idsStringArray := []string{idsString}
counter := 1
for scanner.Next() {
var id string
err := scanner.Scan(&id)
if err == nil {
idsString += fmt.Sprintf("'%s', ", id)
}
counter++
if counter > 100 {
idsStringArray = append(idsStringArray, idsString)
counter = 1
idsString = ""
} else {
// update the last index of array when count is less than 100
idsStringArray[len(idsStringArray)-1] = idsString
}
}
for _, idStr := range idsStringArray {
idStr = strings.Trim(idStr, " ")
idStr = strings.TrimSuffix(idStr, ",")
query = fmt.Sprintf("UPDATE %s SET %s WHERE id IN (%s)", KeySpace+"."+models.Collections.User, updateFields, idStr)
err := p.db.Query(query).Exec()
if err != nil {
return err
}
}
}
return nil
}

View File

@ -0,0 +1,70 @@
package mongodb
import (
"context"
"time"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) {
otp, _ := p.GetOTPByEmail(ctx, otpParam.Email)
shouldCreate := false
if otp == nil {
id := uuid.NewString()
otp = &models.OTP{
ID: id,
Key: id,
Otp: otpParam.Otp,
Email: otpParam.Email,
ExpiresAt: otpParam.ExpiresAt,
CreatedAt: time.Now().Unix(),
}
shouldCreate = true
} else {
otp.Otp = otpParam.Otp
otp.ExpiresAt = otpParam.ExpiresAt
}
otp.UpdatedAt = time.Now().Unix()
otpCollection := p.db.Collection(models.Collections.OTP, options.Collection())
var err error
if shouldCreate {
_, err = otpCollection.InsertOne(ctx, otp)
} else {
_, err = otpCollection.UpdateOne(ctx, bson.M{"_id": bson.M{"$eq": otp.ID}}, bson.M{"$set": otp}, options.MergeUpdateOptions())
}
if err != nil {
return nil, err
}
return otp, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
var otp models.OTP
otpCollection := p.db.Collection(models.Collections.OTP, options.Collection())
err := otpCollection.FindOne(ctx, bson.M{"email": emailAddress}).Decode(&otp)
if err != nil {
return nil, err
}
return &otp, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
otpCollection := p.db.Collection(models.Collections.OTP, options.Collection())
_, err := otpCollection.DeleteOne(nil, bson.M{"_id": otp.ID}, options.Delete())
if err != nil {
return err
}
return nil
}

View File

@ -110,6 +110,15 @@ func NewProvider() (*provider, error) {
}, },
}, options.CreateIndexes()) }, options.CreateIndexes())
mongodb.CreateCollection(ctx, models.Collections.OTP, options.CreateCollection())
otpCollection := mongodb.Collection(models.Collections.OTP, options.Collection())
otpCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
{
Keys: bson.M{"email": 1},
Options: options.Index().SetUnique(true).SetSparse(true),
},
}, options.CreateIndexes())
return &provider{ return &provider{
db: mongodb, db: mongodb,
}, nil }, nil

View File

@ -9,7 +9,9 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/google/uuid" "github.com/google/uuid"
log "github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/options"
) )
@ -129,3 +131,27 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
return user, nil return user, nil
} }
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
userCollection := p.db.Collection(models.Collections.User, options.Collection())
var res *mongo.UpdateResult
var err error
if ids != nil && len(ids) > 0 {
res, err = userCollection.UpdateMany(ctx, bson.M{"_id": bson.M{"$in": ids}}, bson.M{"$set": data})
} else {
res, err = userCollection.UpdateMany(ctx, bson.M{}, bson.M{"$set": data})
}
if err != nil {
return err
} else {
log.Info("Updated users: ", res.ModifiedCount)
}
return nil
}

View File

@ -0,0 +1,22 @@
package provider_template
import (
"context"
"github.com/authorizerdev/authorizer/server/db/models"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) {
return nil, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
return nil, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
return nil
}

View File

@ -60,3 +60,12 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
return user, nil return user, nil
} }
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
return nil
}

View File

@ -20,6 +20,9 @@ type Provider interface {
GetUserByEmail(ctx context.Context, email string) (models.User, error) GetUserByEmail(ctx context.Context, email string) (models.User, error)
// GetUserByID to get user information from database using user ID // GetUserByID to get user information from database using user ID
GetUserByID(ctx context.Context, id string) (models.User, error) GetUserByID(ctx context.Context, id string) (models.User, error)
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error
// AddVerification to save verification request in database // AddVerification to save verification request in database
AddVerificationRequest(ctx context.Context, verificationRequest models.VerificationRequest) (models.VerificationRequest, error) AddVerificationRequest(ctx context.Context, verificationRequest models.VerificationRequest) (models.VerificationRequest, error)
@ -72,4 +75,11 @@ type Provider interface {
GetEmailTemplateByEventName(ctx context.Context, eventName string) (*model.EmailTemplate, error) GetEmailTemplateByEventName(ctx context.Context, eventName string) (*model.EmailTemplate, error)
// DeleteEmailTemplate to delete EmailTemplate // DeleteEmailTemplate to delete EmailTemplate
DeleteEmailTemplate(ctx context.Context, emailTemplate *model.EmailTemplate) error DeleteEmailTemplate(ctx context.Context, emailTemplate *model.EmailTemplate) error
// UpsertOTP to add or update otp
UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error)
// GetOTPByEmail to get otp for a given email address
GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error)
// DeleteOTP to delete otp
DeleteOTP(ctx context.Context, otp *models.OTP) error
} }

View File

@ -0,0 +1,53 @@
package sql
import (
"context"
"time"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/google/uuid"
"gorm.io/gorm/clause"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) {
if otp.ID == "" {
otp.ID = uuid.New().String()
}
otp.Key = otp.ID
otp.CreatedAt = time.Now().Unix()
otp.UpdatedAt = time.Now().Unix()
res := p.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}},
DoUpdates: clause.AssignmentColumns([]string{"otp", "expires_at", "updated_at"}),
}).Create(&otp)
if res.Error != nil {
return nil, res.Error
}
return otp, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
var otp models.OTP
result := p.db.Where("email = ?", emailAddress).First(&otp)
if result.Error != nil {
return nil, result.Error
}
return &otp, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
result := p.db.Delete(&models.OTP{
ID: otp.ID,
})
if result.Error != nil {
return result.Error
}
return nil
}

View File

@ -40,6 +40,7 @@ func NewProvider() (*provider, error) {
NamingStrategy: schema.NamingStrategy{ NamingStrategy: schema.NamingStrategy{
TablePrefix: models.Prefix, TablePrefix: models.Prefix,
}, },
AllowGlobalUpdate: true,
} }
dbType := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseType dbType := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseType
@ -60,7 +61,7 @@ func NewProvider() (*provider, error) {
return nil, err return nil, err
} }
err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, models.WebhookLog{}, models.EmailTemplate{}) err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, models.WebhookLog{}, models.EmailTemplate{}, &models.OTP{})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
@ -121,3 +122,22 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
return user, nil return user, nil
} }
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
var res *gorm.DB
if ids != nil && len(ids) > 0 {
res = p.db.Model(&models.User{}).Where("id in ?", ids).Updates(data)
} else {
res = p.db.Model(&models.User{}).Updates(data)
}
if res.Error != nil {
return res.Error
}
return nil
}

118
server/email/otp.go Normal file
View File

@ -0,0 +1,118 @@
package email
import (
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/memorystore"
)
// SendOtpMail to send otp email
func SendOtpMail(toEmail, otp string) error {
// The receiver needs to be in slice as the receive supports multiple receiver
Receiver := []string{toEmail}
Subject := "OTP for your multi factor authentication"
message := `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta name="x-apple-disable-message-reformatting">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="telephone=no" name="format-detection">
<title></title>
<!--[if (mso 16)]>
<style type="text/css">
a {}
</style>
<![endif]-->
<!--[if gte mso 9]><style>sup { font-size: 100%% !important; }</style><![endif]-->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG></o:AllowPNG>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
</head>
<body style="font-family: sans-serif;">
<div class="es-wrapper-color">
<!--[if gte mso 9]>
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
<v:fill type="tile" color="#ffffff"></v:fill>
</v:background>
<![endif]-->
<table class="es-wrapper" width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-email-paddings" valign="top">
<table class="es-content esd-footer-popover" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr>
<td class="esd-stripe" align="center">
<table class="es-content-body" style="border-left:1px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent;border-bottom:1px solid transparent;padding:20px 0px;" width="600" cellspacing="0" cellpadding="0" bgcolor="#ffffff" align="center">
<tbody>
<tr>
<td class="esd-structure es-p20t es-p40b es-p40r es-p40l" esd-custom-block-id="8537" align="left">
<table width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-container-frame" width="518" align="left">
<table width="100%%" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank" clicktracking="off"><img src="{{.org_logo}}" alt="icon" style="display: block;" title="icon" width="30"></a></td>
</tr>
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
<p>Hey there 👋</p>
<b>{{.otp}}</b> is your one time password (OTP) for accessing {{.org_name}}. Please keep your OTP confidential and it will expire in 1 minute.
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<div style="position: absolute; left: -9999px; top: -9999px; margin: 0px;"></div>
</body>
</html>
`
data := make(map[string]interface{}, 3)
var err error
data["org_logo"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
if err != nil {
return err
}
data["org_name"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
if err != nil {
return err
}
data["otp"] = otp
message = addEmailTemplate(message, data, "otp.tmpl")
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
err = SendMail(Receiver, Subject, message)
if err != nil {
log.Warn("error sending email: ", err)
}
return err
}

41
server/env/env.go vendored
View File

@ -84,6 +84,8 @@ func InitAllEnv() error {
osDisableSignUp := os.Getenv(constants.EnvKeyDisableSignUp) osDisableSignUp := os.Getenv(constants.EnvKeyDisableSignUp)
osDisableRedisForEnv := os.Getenv(constants.EnvKeyDisableRedisForEnv) osDisableRedisForEnv := os.Getenv(constants.EnvKeyDisableRedisForEnv)
osDisableStrongPassword := os.Getenv(constants.EnvKeyDisableStrongPassword) osDisableStrongPassword := os.Getenv(constants.EnvKeyDisableStrongPassword)
osEnforceMultiFactorAuthentication := os.Getenv(constants.EnvKeyEnforceMultiFactorAuthentication)
osDisableMultiFactorAuthentication := os.Getenv(constants.EnvKeyDisableMultiFactorAuthentication)
// os slice vars // os slice vars
osAllowedOrigins := os.Getenv(constants.EnvKeyAllowedOrigins) osAllowedOrigins := os.Getenv(constants.EnvKeyAllowedOrigins)
@ -490,10 +492,49 @@ func InitAllEnv() error {
} }
} }
if _, ok := envData[constants.EnvKeyEnforceMultiFactorAuthentication]; !ok {
envData[constants.EnvKeyEnforceMultiFactorAuthentication] = osEnforceMultiFactorAuthentication == "true"
}
if osEnforceMultiFactorAuthentication != "" {
boolValue, err := strconv.ParseBool(osEnforceMultiFactorAuthentication)
if err != nil {
return err
}
if boolValue != envData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) {
envData[constants.EnvKeyEnforceMultiFactorAuthentication] = boolValue
}
}
if _, ok := envData[constants.EnvKeyDisableMultiFactorAuthentication]; !ok {
envData[constants.EnvKeyDisableMultiFactorAuthentication] = osDisableMultiFactorAuthentication == "true"
}
if osDisableMultiFactorAuthentication != "" {
boolValue, err := strconv.ParseBool(osDisableMultiFactorAuthentication)
if err != nil {
return err
}
if boolValue != envData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) {
envData[constants.EnvKeyDisableMultiFactorAuthentication] = boolValue
}
}
// no need to add nil check as its already done above // no need to add nil check as its already done above
if envData[constants.EnvKeySmtpHost] == "" || envData[constants.EnvKeySmtpUsername] == "" || envData[constants.EnvKeySmtpPassword] == "" || envData[constants.EnvKeySenderEmail] == "" && envData[constants.EnvKeySmtpPort] == "" { if envData[constants.EnvKeySmtpHost] == "" || envData[constants.EnvKeySmtpUsername] == "" || envData[constants.EnvKeySmtpPassword] == "" || envData[constants.EnvKeySenderEmail] == "" && envData[constants.EnvKeySmtpPort] == "" {
envData[constants.EnvKeyDisableEmailVerification] = true envData[constants.EnvKeyDisableEmailVerification] = true
envData[constants.EnvKeyDisableMagicLinkLogin] = true envData[constants.EnvKeyDisableMagicLinkLogin] = true
envData[constants.EnvKeyIsEmailServiceEnabled] = false
}
if envData[constants.EnvKeySmtpHost] != "" || envData[constants.EnvKeySmtpUsername] != "" || envData[constants.EnvKeySmtpPassword] != "" || envData[constants.EnvKeySenderEmail] != "" && envData[constants.EnvKeySmtpPort] != "" {
envData[constants.EnvKeyIsEmailServiceEnabled] = true
}
if envData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && !envData[constants.EnvKeyIsEmailServiceEnabled].(bool) {
return errors.New("to enable multi factor authentication, please enable email service")
}
if !envData[constants.EnvKeyIsEmailServiceEnabled].(bool) {
envData[constants.EnvKeyDisableMultiFactorAuthentication] = true
} }
if envData[constants.EnvKeyDisableEmailVerification].(bool) { if envData[constants.EnvKeyDisableEmailVerification].(bool) {

View File

@ -201,7 +201,7 @@ func PersistEnv() error {
envValue := strings.TrimSpace(os.Getenv(key)) envValue := strings.TrimSpace(os.Getenv(key))
if envValue != "" { if envValue != "" {
switch key { switch key {
case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword: case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication, constants.EnvKeyDisableMultiFactorAuthentication:
if envValueBool, err := strconv.ParseBool(envValue); err == nil { if envValueBool, err := strconv.ParseBool(envValue); err == nil {
if value.(bool) != envValueBool { if value.(bool) != envValueBool {
storeData[key] = envValueBool storeData[key] = envValueBool
@ -221,6 +221,8 @@ func PersistEnv() error {
// handle derivative cases like disabling email verification & magic login // handle derivative cases like disabling email verification & magic login
// in case SMTP is off but env is set to true // in case SMTP is off but env is set to true
if storeData[constants.EnvKeySmtpHost] == "" || storeData[constants.EnvKeySmtpUsername] == "" || storeData[constants.EnvKeySmtpPassword] == "" || storeData[constants.EnvKeySenderEmail] == "" && storeData[constants.EnvKeySmtpPort] == "" { if storeData[constants.EnvKeySmtpHost] == "" || storeData[constants.EnvKeySmtpUsername] == "" || storeData[constants.EnvKeySmtpPassword] == "" || storeData[constants.EnvKeySenderEmail] == "" && storeData[constants.EnvKeySmtpPort] == "" {
storeData[constants.EnvKeyIsEmailServiceEnabled] = false
if !storeData[constants.EnvKeyDisableEmailVerification].(bool) { if !storeData[constants.EnvKeyDisableEmailVerification].(bool) {
storeData[constants.EnvKeyDisableEmailVerification] = true storeData[constants.EnvKeyDisableEmailVerification] = true
hasChanged = true hasChanged = true

View File

@ -9,7 +9,7 @@ require (
github.com/gin-gonic/gin v1.7.2 github.com/gin-gonic/gin v1.7.2
github.com/go-playground/validator/v10 v10.8.0 // indirect github.com/go-playground/validator/v10 v10.8.0 // indirect
github.com/go-redis/redis/v8 v8.11.0 github.com/go-redis/redis/v8 v8.11.0
github.com/gocql/gocql v1.0.0 github.com/gocql/gocql v1.2.0
github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0

View File

@ -110,8 +110,8 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gocql/gocql v1.0.0 h1:UnbTERpP72VZ/viKE1Q1gPtmLvyTZTvuAstvSRydw/c= github.com/gocql/gocql v1.2.0 h1:TZhsCd7fRuye4VyHr3WCvWwIQaZUmjsqnSIXK9FcVCE=
github.com/gocql/gocql v1.0.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= github.com/gocql/gocql v1.2.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=

View File

@ -49,6 +49,7 @@ type ComplexityRoot struct {
IDToken func(childComplexity int) int IDToken func(childComplexity int) int
Message func(childComplexity int) int Message func(childComplexity int) int
RefreshToken func(childComplexity int) int RefreshToken func(childComplexity int) int
ShouldShowOtpScreen func(childComplexity int) int
User func(childComplexity int) int User func(childComplexity int) int
} }
@ -88,9 +89,11 @@ type ComplexityRoot struct {
DisableEmailVerification func(childComplexity int) int DisableEmailVerification func(childComplexity int) int
DisableLoginPage func(childComplexity int) int DisableLoginPage func(childComplexity int) int
DisableMagicLinkLogin func(childComplexity int) int DisableMagicLinkLogin func(childComplexity int) int
DisableMultiFactorAuthentication func(childComplexity int) int
DisableRedisForEnv func(childComplexity int) int DisableRedisForEnv func(childComplexity int) int
DisableSignUp func(childComplexity int) int DisableSignUp func(childComplexity int) int
DisableStrongPassword func(childComplexity int) int DisableStrongPassword func(childComplexity int) int
EnforceMultiFactorAuthentication func(childComplexity int) int
FacebookClientID func(childComplexity int) int FacebookClientID func(childComplexity int) int
FacebookClientSecret func(childComplexity int) int FacebookClientSecret func(childComplexity int) int
GithubClientID func(childComplexity int) int GithubClientID func(childComplexity int) int
@ -138,6 +141,7 @@ type ComplexityRoot struct {
IsGoogleLoginEnabled func(childComplexity int) int IsGoogleLoginEnabled func(childComplexity int) int
IsLinkedinLoginEnabled func(childComplexity int) int IsLinkedinLoginEnabled func(childComplexity int) int
IsMagicLinkLoginEnabled func(childComplexity int) int IsMagicLinkLoginEnabled func(childComplexity int) int
IsMultiFactorAuthEnabled func(childComplexity int) int
IsSignUpEnabled func(childComplexity int) int IsSignUpEnabled func(childComplexity int) int
IsStrongPasswordEnabled func(childComplexity int) int IsStrongPasswordEnabled func(childComplexity int) int
Version func(childComplexity int) int Version func(childComplexity int) int
@ -159,6 +163,7 @@ type ComplexityRoot struct {
Login func(childComplexity int, params model.LoginInput) int Login func(childComplexity int, params model.LoginInput) int
Logout func(childComplexity int) int Logout func(childComplexity int) int
MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int
ResendOtp func(childComplexity int, params model.ResendOTPRequest) int
ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int
ResetPassword func(childComplexity int, params model.ResetPasswordInput) int ResetPassword func(childComplexity int, params model.ResetPasswordInput) int
Revoke func(childComplexity int, params model.OAuthRevokeInput) int Revoke func(childComplexity int, params model.OAuthRevokeInput) int
@ -171,6 +176,7 @@ type ComplexityRoot struct {
UpdateUser func(childComplexity int, params model.UpdateUserInput) int UpdateUser func(childComplexity int, params model.UpdateUserInput) int
UpdateWebhook func(childComplexity int, params model.UpdateWebhookRequest) int UpdateWebhook func(childComplexity int, params model.UpdateWebhookRequest) int
VerifyEmail func(childComplexity int, params model.VerifyEmailInput) int VerifyEmail func(childComplexity int, params model.VerifyEmailInput) int
VerifyOtp func(childComplexity int, params model.VerifyOTPRequest) int
} }
Pagination struct { Pagination struct {
@ -213,6 +219,7 @@ type ComplexityRoot struct {
Gender func(childComplexity int) int Gender func(childComplexity int) int
GivenName func(childComplexity int) int GivenName func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
IsMultiFactorAuthEnabled func(childComplexity int) int
MiddleName func(childComplexity int) int MiddleName func(childComplexity int) int
Nickname func(childComplexity int) int Nickname func(childComplexity int) int
PhoneNumber func(childComplexity int) int PhoneNumber func(childComplexity int) int
@ -293,6 +300,8 @@ type MutationResolver interface {
ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error) ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error)
ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error) ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error)
Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error)
VerifyOtp(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error)
ResendOtp(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error)
DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error)
UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error) UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error)
AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error) AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error)
@ -376,6 +385,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.AuthResponse.RefreshToken(childComplexity), true return e.complexity.AuthResponse.RefreshToken(childComplexity), true
case "AuthResponse.should_show_otp_screen":
if e.complexity.AuthResponse.ShouldShowOtpScreen == nil {
break
}
return e.complexity.AuthResponse.ShouldShowOtpScreen(childComplexity), true
case "AuthResponse.user": case "AuthResponse.user":
if e.complexity.AuthResponse.User == nil { if e.complexity.AuthResponse.User == nil {
break break
@ -586,6 +602,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Env.DisableMagicLinkLogin(childComplexity), true return e.complexity.Env.DisableMagicLinkLogin(childComplexity), true
case "Env.DISABLE_MULTI_FACTOR_AUTHENTICATION":
if e.complexity.Env.DisableMultiFactorAuthentication == nil {
break
}
return e.complexity.Env.DisableMultiFactorAuthentication(childComplexity), true
case "Env.DISABLE_REDIS_FOR_ENV": case "Env.DISABLE_REDIS_FOR_ENV":
if e.complexity.Env.DisableRedisForEnv == nil { if e.complexity.Env.DisableRedisForEnv == nil {
break break
@ -607,6 +630,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Env.DisableStrongPassword(childComplexity), true return e.complexity.Env.DisableStrongPassword(childComplexity), true
case "Env.ENFORCE_MULTI_FACTOR_AUTHENTICATION":
if e.complexity.Env.EnforceMultiFactorAuthentication == nil {
break
}
return e.complexity.Env.EnforceMultiFactorAuthentication(childComplexity), true
case "Env.FACEBOOK_CLIENT_ID": case "Env.FACEBOOK_CLIENT_ID":
if e.complexity.Env.FacebookClientID == nil { if e.complexity.Env.FacebookClientID == nil {
break break
@ -873,6 +903,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Meta.IsMagicLinkLoginEnabled(childComplexity), true return e.complexity.Meta.IsMagicLinkLoginEnabled(childComplexity), true
case "Meta.is_multi_factor_auth_enabled":
if e.complexity.Meta.IsMultiFactorAuthEnabled == nil {
break
}
return e.complexity.Meta.IsMultiFactorAuthEnabled(childComplexity), true
case "Meta.is_sign_up_enabled": case "Meta.is_sign_up_enabled":
if e.complexity.Meta.IsSignUpEnabled == nil { if e.complexity.Meta.IsSignUpEnabled == nil {
break break
@ -1064,6 +1101,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.MagicLinkLogin(childComplexity, args["params"].(model.MagicLinkLoginInput)), true return e.complexity.Mutation.MagicLinkLogin(childComplexity, args["params"].(model.MagicLinkLoginInput)), true
case "Mutation.resend_otp":
if e.complexity.Mutation.ResendOtp == nil {
break
}
args, err := ec.field_Mutation_resend_otp_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.ResendOtp(childComplexity, args["params"].(model.ResendOTPRequest)), true
case "Mutation.resend_verify_email": case "Mutation.resend_verify_email":
if e.complexity.Mutation.ResendVerifyEmail == nil { if e.complexity.Mutation.ResendVerifyEmail == nil {
break break
@ -1208,6 +1257,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.VerifyEmail(childComplexity, args["params"].(model.VerifyEmailInput)), true return e.complexity.Mutation.VerifyEmail(childComplexity, args["params"].(model.VerifyEmailInput)), true
case "Mutation.verify_otp":
if e.complexity.Mutation.VerifyOtp == nil {
break
}
args, err := ec.field_Mutation_verify_otp_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.VerifyOtp(childComplexity, args["params"].(model.VerifyOTPRequest)), true
case "Pagination.limit": case "Pagination.limit":
if e.complexity.Pagination.Limit == nil { if e.complexity.Pagination.Limit == nil {
break break
@ -1437,6 +1498,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.User.ID(childComplexity), true return e.complexity.User.ID(childComplexity), true
case "User.is_multi_factor_auth_enabled":
if e.complexity.User.IsMultiFactorAuthEnabled == nil {
break
}
return e.complexity.User.IsMultiFactorAuthEnabled(childComplexity), true
case "User.middle_name": case "User.middle_name":
if e.complexity.User.MiddleName == nil { if e.complexity.User.MiddleName == nil {
break break
@ -1822,6 +1890,7 @@ type Meta {
is_magic_link_login_enabled: Boolean! is_magic_link_login_enabled: Boolean!
is_sign_up_enabled: Boolean! is_sign_up_enabled: Boolean!
is_strong_password_enabled: Boolean! is_strong_password_enabled: Boolean!
is_multi_factor_auth_enabled: Boolean!
} }
type User { type User {
@ -1844,6 +1913,7 @@ type User {
created_at: Int64 created_at: Int64
updated_at: Int64 updated_at: Int64
revoked_timestamp: Int64 revoked_timestamp: Int64
is_multi_factor_auth_enabled: Boolean
} }
type Users { type Users {
@ -1875,6 +1945,7 @@ type Error {
type AuthResponse { type AuthResponse {
message: String! message: String!
should_show_otp_screen: Boolean
access_token: String access_token: String
id_token: String id_token: String
refresh_token: String refresh_token: String
@ -1919,6 +1990,8 @@ type Env {
DISABLE_SIGN_UP: Boolean! DISABLE_SIGN_UP: Boolean!
DISABLE_REDIS_FOR_ENV: Boolean! DISABLE_REDIS_FOR_ENV: Boolean!
DISABLE_STRONG_PASSWORD: Boolean! DISABLE_STRONG_PASSWORD: Boolean!
DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean!
ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean!
ROLES: [String!] ROLES: [String!]
PROTECTED_ROLES: [String!] PROTECTED_ROLES: [String!]
DEFAULT_ROLES: [String!] DEFAULT_ROLES: [String!]
@ -2020,6 +2093,8 @@ input UpdateEnvInput {
DISABLE_SIGN_UP: Boolean DISABLE_SIGN_UP: Boolean
DISABLE_REDIS_FOR_ENV: Boolean DISABLE_REDIS_FOR_ENV: Boolean
DISABLE_STRONG_PASSWORD: Boolean DISABLE_STRONG_PASSWORD: Boolean
DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean
ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean
ROLES: [String!] ROLES: [String!]
PROTECTED_ROLES: [String!] PROTECTED_ROLES: [String!]
DEFAULT_ROLES: [String!] DEFAULT_ROLES: [String!]
@ -2061,6 +2136,7 @@ input SignUpInput {
roles: [String!] roles: [String!]
scope: [String!] scope: [String!]
redirect_uri: String redirect_uri: String
is_multi_factor_auth_enabled: Boolean
} }
input LoginInput { input LoginInput {
@ -2092,6 +2168,7 @@ input UpdateProfileInput {
birthdate: String birthdate: String
phone_number: String phone_number: String
picture: String picture: String
is_multi_factor_auth_enabled: Boolean
} }
input UpdateUserInput { input UpdateUserInput {
@ -2107,6 +2184,7 @@ input UpdateUserInput {
phone_number: String phone_number: String
picture: String picture: String
roles: [String] roles: [String]
is_multi_factor_auth_enabled: Boolean
} }
input ForgotPasswordInput { input ForgotPasswordInput {
@ -2217,6 +2295,15 @@ input DeleteEmailTemplateRequest {
id: ID! id: ID!
} }
input VerifyOTPRequest {
email: String!
otp: String!
}
input ResendOTPRequest {
email: String!
}
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
@ -2228,6 +2315,8 @@ type Mutation {
forgot_password(params: ForgotPasswordInput!): Response! forgot_password(params: ForgotPasswordInput!): Response!
reset_password(params: ResetPasswordInput!): Response! reset_password(params: ResetPasswordInput!): Response!
revoke(params: OAuthRevokeInput!): Response! revoke(params: OAuthRevokeInput!): Response!
verify_otp(params: VerifyOTPRequest!): AuthResponse!
resend_otp(params: ResendOTPRequest!): Response!
# admin only apis # admin only apis
_delete_user(params: DeleteUserInput!): Response! _delete_user(params: DeleteUserInput!): Response!
_update_user(params: UpdateUserInput!): User! _update_user(params: UpdateUserInput!): User!
@ -2556,6 +2645,21 @@ func (ec *executionContext) field_Mutation_magic_link_login_args(ctx context.Con
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation_resend_otp_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 model.ResendOTPRequest
if tmp, ok := rawArgs["params"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params"))
arg0, err = ec.unmarshalNResendOTPRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResendOTPRequest(ctx, tmp)
if err != nil {
return nil, err
}
}
args["params"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_resend_verify_email_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Mutation_resend_verify_email_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@ -2646,6 +2750,21 @@ func (ec *executionContext) field_Mutation_verify_email_args(ctx context.Context
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation_verify_otp_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 model.VerifyOTPRequest
if tmp, ok := rawArgs["params"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params"))
arg0, err = ec.unmarshalNVerifyOTPRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐVerifyOTPRequest(ctx, tmp)
if err != nil {
return nil, err
}
}
args["params"] = arg0
return args, nil
}
func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@ -2854,6 +2973,38 @@ func (ec *executionContext) _AuthResponse_message(ctx context.Context, field gra
return ec.marshalNString2string(ctx, field.Selections, res) return ec.marshalNString2string(ctx, field.Selections, res)
} }
func (ec *executionContext) _AuthResponse_should_show_otp_screen(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "AuthResponse",
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.ShouldShowOtpScreen, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*bool)
fc.Result = res
return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res)
}
func (ec *executionContext) _AuthResponse_access_token(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { func (ec *executionContext) _AuthResponse_access_token(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -4339,6 +4490,76 @@ func (ec *executionContext) _Env_DISABLE_STRONG_PASSWORD(ctx context.Context, fi
return ec.marshalNBoolean2bool(ctx, field.Selections, res) return ec.marshalNBoolean2bool(ctx, field.Selections, res)
} }
func (ec *executionContext) _Env_DISABLE_MULTI_FACTOR_AUTHENTICATION(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.DisableMultiFactorAuthentication, 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) _Env_ENFORCE_MULTI_FACTOR_AUTHENTICATION(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.EnforceMultiFactorAuthentication, 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) _Env_ROLES(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { func (ec *executionContext) _Env_ROLES(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 {
@ -5437,6 +5658,41 @@ func (ec *executionContext) _Meta_is_strong_password_enabled(ctx context.Context
return ec.marshalNBoolean2bool(ctx, field.Selections, res) return ec.marshalNBoolean2bool(ctx, field.Selections, res)
} }
func (ec *executionContext) _Meta_is_multi_factor_auth_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.IsMultiFactorAuthEnabled, 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) _Mutation_signup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Mutation_signup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -5850,6 +6106,90 @@ func (ec *executionContext) _Mutation_revoke(ctx context.Context, field graphql.
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
} }
func (ec *executionContext) _Mutation_verify_otp(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_verify_otp_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().VerifyOtp(rctx, args["params"].(model.VerifyOTPRequest))
})
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.(*model.AuthResponse)
fc.Result = res
return ec.marshalNAuthResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAuthResponse(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_resend_otp(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_resend_otp_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().ResendOtp(rctx, args["params"].(model.ResendOTPRequest))
})
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.(*model.Response)
fc.Result = res
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -7934,6 +8274,38 @@ func (ec *executionContext) _User_revoked_timestamp(ctx context.Context, field g
return ec.marshalOInt642ᚖint64(ctx, field.Selections, res) return ec.marshalOInt642ᚖint64(ctx, field.Selections, res)
} }
func (ec *executionContext) _User_is_multi_factor_auth_enabled(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "User",
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.IsMultiFactorAuthEnabled, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*bool)
fc.Result = res
return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res)
}
func (ec *executionContext) _Users_pagination(ctx context.Context, field graphql.CollectedField, obj *model.Users) (ret graphql.Marshaler) { func (ec *executionContext) _Users_pagination(ctx context.Context, field graphql.CollectedField, obj *model.Users) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -10597,6 +10969,29 @@ func (ec *executionContext) unmarshalInputPaginationInput(ctx context.Context, o
return it, nil return it, nil
} }
func (ec *executionContext) unmarshalInputResendOTPRequest(ctx context.Context, obj interface{}) (model.ResendOTPRequest, error) {
var it model.ResendOTPRequest
asMap := map[string]interface{}{}
for k, v := range obj.(map[string]interface{}) {
asMap[k] = v
}
for k, v := range asMap {
switch k {
case "email":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email"))
it.Email, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputResendVerifyEmailInput(ctx context.Context, obj interface{}) (model.ResendVerifyEmailInput, error) { func (ec *executionContext) unmarshalInputResendVerifyEmailInput(ctx context.Context, obj interface{}) (model.ResendVerifyEmailInput, error) {
var it model.ResendVerifyEmailInput var it model.ResendVerifyEmailInput
asMap := map[string]interface{}{} asMap := map[string]interface{}{}
@ -10819,6 +11214,14 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i
if err != nil { if err != nil {
return it, err return it, err
} }
case "is_multi_factor_auth_enabled":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("is_multi_factor_auth_enabled"))
it.IsMultiFactorAuthEnabled, err = ec.unmarshalOBoolean2ᚖbool(ctx, v)
if err != nil {
return it, err
}
} }
} }
@ -11127,6 +11530,22 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob
if err != nil { if err != nil {
return it, err return it, err
} }
case "DISABLE_MULTI_FACTOR_AUTHENTICATION":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("DISABLE_MULTI_FACTOR_AUTHENTICATION"))
it.DisableMultiFactorAuthentication, err = ec.unmarshalOBoolean2ᚖbool(ctx, v)
if err != nil {
return it, err
}
case "ENFORCE_MULTI_FACTOR_AUTHENTICATION":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ENFORCE_MULTI_FACTOR_AUTHENTICATION"))
it.EnforceMultiFactorAuthentication, err = ec.unmarshalOBoolean2ᚖbool(ctx, v)
if err != nil {
return it, err
}
case "ROLES": case "ROLES":
var err error var err error
@ -11366,6 +11785,14 @@ func (ec *executionContext) unmarshalInputUpdateProfileInput(ctx context.Context
if err != nil { if err != nil {
return it, err return it, err
} }
case "is_multi_factor_auth_enabled":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("is_multi_factor_auth_enabled"))
it.IsMultiFactorAuthEnabled, err = ec.unmarshalOBoolean2ᚖbool(ctx, v)
if err != nil {
return it, err
}
} }
} }
@ -11477,6 +11904,14 @@ func (ec *executionContext) unmarshalInputUpdateUserInput(ctx context.Context, o
if err != nil { if err != nil {
return it, err return it, err
} }
case "is_multi_factor_auth_enabled":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("is_multi_factor_auth_enabled"))
it.IsMultiFactorAuthEnabled, err = ec.unmarshalOBoolean2ᚖbool(ctx, v)
if err != nil {
return it, err
}
} }
} }
@ -11600,6 +12035,37 @@ func (ec *executionContext) unmarshalInputVerifyEmailInput(ctx context.Context,
return it, nil return it, nil
} }
func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context, obj interface{}) (model.VerifyOTPRequest, error) {
var it model.VerifyOTPRequest
asMap := map[string]interface{}{}
for k, v := range obj.(map[string]interface{}) {
asMap[k] = v
}
for k, v := range asMap {
switch k {
case "email":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email"))
it.Email, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
case "otp":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("otp"))
it.Otp, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputWebhookRequest(ctx context.Context, obj interface{}) (model.WebhookRequest, error) { func (ec *executionContext) unmarshalInputWebhookRequest(ctx context.Context, obj interface{}) (model.WebhookRequest, error) {
var it model.WebhookRequest var it model.WebhookRequest
asMap := map[string]interface{}{} asMap := map[string]interface{}{}
@ -11647,6 +12113,8 @@ func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.Selection
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "should_show_otp_screen":
out.Values[i] = ec._AuthResponse_should_show_otp_screen(ctx, field, obj)
case "access_token": case "access_token":
out.Values[i] = ec._AuthResponse_access_token(ctx, field, obj) out.Values[i] = ec._AuthResponse_access_token(ctx, field, obj)
case "id_token": case "id_token":
@ -11848,6 +12316,16 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "DISABLE_MULTI_FACTOR_AUTHENTICATION":
out.Values[i] = ec._Env_DISABLE_MULTI_FACTOR_AUTHENTICATION(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "ENFORCE_MULTI_FACTOR_AUTHENTICATION":
out.Values[i] = ec._Env_ENFORCE_MULTI_FACTOR_AUTHENTICATION(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "ROLES": case "ROLES":
out.Values[i] = ec._Env_ROLES(ctx, field, obj) out.Values[i] = ec._Env_ROLES(ctx, field, obj)
case "PROTECTED_ROLES": case "PROTECTED_ROLES":
@ -12022,6 +12500,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_multi_factor_auth_enabled":
out.Values[i] = ec._Meta_is_multi_factor_auth_enabled(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@ -12098,6 +12581,16 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "verify_otp":
out.Values[i] = ec._Mutation_verify_otp(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "resend_otp":
out.Values[i] = ec._Mutation_resend_otp(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "_delete_user": case "_delete_user":
out.Values[i] = ec._Mutation__delete_user(ctx, field) out.Values[i] = ec._Mutation__delete_user(ctx, field)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@ -12549,6 +13042,8 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
out.Values[i] = ec._User_updated_at(ctx, field, obj) out.Values[i] = ec._User_updated_at(ctx, field, obj)
case "revoked_timestamp": case "revoked_timestamp":
out.Values[i] = ec._User_revoked_timestamp(ctx, field, obj) out.Values[i] = ec._User_revoked_timestamp(ctx, field, obj)
case "is_multi_factor_auth_enabled":
out.Values[i] = ec._User_is_multi_factor_auth_enabled(ctx, field, obj)
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@ -13325,6 +13820,11 @@ func (ec *executionContext) marshalNPagination2ᚖgithubᚗcomᚋauthorizerdev
return ec._Pagination(ctx, sel, v) return ec._Pagination(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNResendOTPRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResendOTPRequest(ctx context.Context, v interface{}) (model.ResendOTPRequest, error) {
res, err := ec.unmarshalInputResendOTPRequest(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) unmarshalNResendVerifyEmailInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResendVerifyEmailInput(ctx context.Context, v interface{}) (model.ResendVerifyEmailInput, error) { func (ec *executionContext) unmarshalNResendVerifyEmailInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResendVerifyEmailInput(ctx context.Context, v interface{}) (model.ResendVerifyEmailInput, error) {
res, err := ec.unmarshalInputResendVerifyEmailInput(ctx, v) res, err := ec.unmarshalInputResendVerifyEmailInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)
@ -13618,6 +14118,11 @@ func (ec *executionContext) unmarshalNVerifyEmailInput2githubᚗcomᚋauthorizer
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)
} }
func (ec *executionContext) unmarshalNVerifyOTPRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐVerifyOTPRequest(ctx context.Context, v interface{}) (model.VerifyOTPRequest, error) {
res, err := ec.unmarshalInputVerifyOTPRequest(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNWebhook2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐWebhook(ctx context.Context, sel ast.SelectionSet, v model.Webhook) graphql.Marshaler { func (ec *executionContext) marshalNWebhook2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐWebhook(ctx context.Context, sel ast.SelectionSet, v model.Webhook) graphql.Marshaler {
return ec._Webhook(ctx, sel, &v) return ec._Webhook(ctx, sel, &v)
} }

View File

@ -25,6 +25,7 @@ type AdminSignupInput struct {
type AuthResponse struct { type AuthResponse struct {
Message string `json:"message"` Message string `json:"message"`
ShouldShowOtpScreen *bool `json:"should_show_otp_screen"`
AccessToken *string `json:"access_token"` AccessToken *string `json:"access_token"`
IDToken *string `json:"id_token"` IDToken *string `json:"id_token"`
RefreshToken *string `json:"refresh_token"` RefreshToken *string `json:"refresh_token"`
@ -87,6 +88,8 @@ type Env struct {
DisableSignUp bool `json:"DISABLE_SIGN_UP"` DisableSignUp bool `json:"DISABLE_SIGN_UP"`
DisableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"` DisableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"`
DisableStrongPassword bool `json:"DISABLE_STRONG_PASSWORD"` DisableStrongPassword bool `json:"DISABLE_STRONG_PASSWORD"`
DisableMultiFactorAuthentication bool `json:"DISABLE_MULTI_FACTOR_AUTHENTICATION"`
EnforceMultiFactorAuthentication bool `json:"ENFORCE_MULTI_FACTOR_AUTHENTICATION"`
Roles []string `json:"ROLES"` Roles []string `json:"ROLES"`
ProtectedRoles []string `json:"PROTECTED_ROLES"` ProtectedRoles []string `json:"PROTECTED_ROLES"`
DefaultRoles []string `json:"DEFAULT_ROLES"` DefaultRoles []string `json:"DEFAULT_ROLES"`
@ -164,6 +167,7 @@ type Meta struct {
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
IsSignUpEnabled bool `json:"is_sign_up_enabled"` IsSignUpEnabled bool `json:"is_sign_up_enabled"`
IsStrongPasswordEnabled bool `json:"is_strong_password_enabled"` IsStrongPasswordEnabled bool `json:"is_strong_password_enabled"`
IsMultiFactorAuthEnabled bool `json:"is_multi_factor_auth_enabled"`
} }
type OAuthRevokeInput struct { type OAuthRevokeInput struct {
@ -186,6 +190,10 @@ type PaginationInput struct {
Page *int64 `json:"page"` Page *int64 `json:"page"`
} }
type ResendOTPRequest struct {
Email string `json:"email"`
}
type ResendVerifyEmailInput struct { type ResendVerifyEmailInput struct {
Email string `json:"email"` Email string `json:"email"`
Identifier string `json:"identifier"` Identifier string `json:"identifier"`
@ -221,6 +229,7 @@ type SignUpInput struct {
Roles []string `json:"roles"` Roles []string `json:"roles"`
Scope []string `json:"scope"` Scope []string `json:"scope"`
RedirectURI *string `json:"redirect_uri"` RedirectURI *string `json:"redirect_uri"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
} }
type TestEndpointRequest struct { type TestEndpointRequest struct {
@ -269,6 +278,8 @@ type UpdateEnvInput struct {
DisableSignUp *bool `json:"DISABLE_SIGN_UP"` DisableSignUp *bool `json:"DISABLE_SIGN_UP"`
DisableRedisForEnv *bool `json:"DISABLE_REDIS_FOR_ENV"` DisableRedisForEnv *bool `json:"DISABLE_REDIS_FOR_ENV"`
DisableStrongPassword *bool `json:"DISABLE_STRONG_PASSWORD"` DisableStrongPassword *bool `json:"DISABLE_STRONG_PASSWORD"`
DisableMultiFactorAuthentication *bool `json:"DISABLE_MULTI_FACTOR_AUTHENTICATION"`
EnforceMultiFactorAuthentication *bool `json:"ENFORCE_MULTI_FACTOR_AUTHENTICATION"`
Roles []string `json:"ROLES"` Roles []string `json:"ROLES"`
ProtectedRoles []string `json:"PROTECTED_ROLES"` ProtectedRoles []string `json:"PROTECTED_ROLES"`
DefaultRoles []string `json:"DEFAULT_ROLES"` DefaultRoles []string `json:"DEFAULT_ROLES"`
@ -300,6 +311,7 @@ type UpdateProfileInput struct {
Birthdate *string `json:"birthdate"` Birthdate *string `json:"birthdate"`
PhoneNumber *string `json:"phone_number"` PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"` Picture *string `json:"picture"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
} }
type UpdateUserInput struct { type UpdateUserInput struct {
@ -315,6 +327,7 @@ type UpdateUserInput struct {
PhoneNumber *string `json:"phone_number"` PhoneNumber *string `json:"phone_number"`
Picture *string `json:"picture"` Picture *string `json:"picture"`
Roles []*string `json:"roles"` Roles []*string `json:"roles"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
} }
type UpdateWebhookRequest struct { type UpdateWebhookRequest struct {
@ -344,6 +357,7 @@ type User struct {
CreatedAt *int64 `json:"created_at"` CreatedAt *int64 `json:"created_at"`
UpdatedAt *int64 `json:"updated_at"` UpdatedAt *int64 `json:"updated_at"`
RevokedTimestamp *int64 `json:"revoked_timestamp"` RevokedTimestamp *int64 `json:"revoked_timestamp"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
} }
type Users struct { type Users struct {
@ -382,6 +396,11 @@ type VerifyEmailInput struct {
Token string `json:"token"` Token string `json:"token"`
} }
type VerifyOTPRequest struct {
Email string `json:"email"`
Otp string `json:"otp"`
}
type Webhook struct { type Webhook struct {
ID string `json:"id"` ID string `json:"id"`
EventName *string `json:"event_name"` EventName *string `json:"event_name"`

View File

@ -25,6 +25,7 @@ type Meta {
is_magic_link_login_enabled: Boolean! is_magic_link_login_enabled: Boolean!
is_sign_up_enabled: Boolean! is_sign_up_enabled: Boolean!
is_strong_password_enabled: Boolean! is_strong_password_enabled: Boolean!
is_multi_factor_auth_enabled: Boolean!
} }
type User { type User {
@ -47,6 +48,7 @@ type User {
created_at: Int64 created_at: Int64
updated_at: Int64 updated_at: Int64
revoked_timestamp: Int64 revoked_timestamp: Int64
is_multi_factor_auth_enabled: Boolean
} }
type Users { type Users {
@ -78,6 +80,7 @@ type Error {
type AuthResponse { type AuthResponse {
message: String! message: String!
should_show_otp_screen: Boolean
access_token: String access_token: String
id_token: String id_token: String
refresh_token: String refresh_token: String
@ -122,6 +125,8 @@ type Env {
DISABLE_SIGN_UP: Boolean! DISABLE_SIGN_UP: Boolean!
DISABLE_REDIS_FOR_ENV: Boolean! DISABLE_REDIS_FOR_ENV: Boolean!
DISABLE_STRONG_PASSWORD: Boolean! DISABLE_STRONG_PASSWORD: Boolean!
DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean!
ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean!
ROLES: [String!] ROLES: [String!]
PROTECTED_ROLES: [String!] PROTECTED_ROLES: [String!]
DEFAULT_ROLES: [String!] DEFAULT_ROLES: [String!]
@ -223,6 +228,8 @@ input UpdateEnvInput {
DISABLE_SIGN_UP: Boolean DISABLE_SIGN_UP: Boolean
DISABLE_REDIS_FOR_ENV: Boolean DISABLE_REDIS_FOR_ENV: Boolean
DISABLE_STRONG_PASSWORD: Boolean DISABLE_STRONG_PASSWORD: Boolean
DISABLE_MULTI_FACTOR_AUTHENTICATION: Boolean
ENFORCE_MULTI_FACTOR_AUTHENTICATION: Boolean
ROLES: [String!] ROLES: [String!]
PROTECTED_ROLES: [String!] PROTECTED_ROLES: [String!]
DEFAULT_ROLES: [String!] DEFAULT_ROLES: [String!]
@ -264,6 +271,7 @@ input SignUpInput {
roles: [String!] roles: [String!]
scope: [String!] scope: [String!]
redirect_uri: String redirect_uri: String
is_multi_factor_auth_enabled: Boolean
} }
input LoginInput { input LoginInput {
@ -295,6 +303,7 @@ input UpdateProfileInput {
birthdate: String birthdate: String
phone_number: String phone_number: String
picture: String picture: String
is_multi_factor_auth_enabled: Boolean
} }
input UpdateUserInput { input UpdateUserInput {
@ -310,6 +319,7 @@ input UpdateUserInput {
phone_number: String phone_number: String
picture: String picture: String
roles: [String] roles: [String]
is_multi_factor_auth_enabled: Boolean
} }
input ForgotPasswordInput { input ForgotPasswordInput {
@ -420,6 +430,15 @@ input DeleteEmailTemplateRequest {
id: ID! id: ID!
} }
input VerifyOTPRequest {
email: String!
otp: String!
}
input ResendOTPRequest {
email: String!
}
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
@ -431,6 +450,8 @@ type Mutation {
forgot_password(params: ForgotPasswordInput!): Response! forgot_password(params: ForgotPasswordInput!): Response!
reset_password(params: ResetPasswordInput!): Response! reset_password(params: ResetPasswordInput!): Response!
revoke(params: OAuthRevokeInput!): Response! revoke(params: OAuthRevokeInput!): Response!
verify_otp(params: VerifyOTPRequest!): AuthResponse!
resend_otp(params: ResendOTPRequest!): Response!
# admin only apis # admin only apis
_delete_user(params: DeleteUserInput!): Response! _delete_user(params: DeleteUserInput!): Response!
_update_user(params: UpdateUserInput!): User! _update_user(params: UpdateUserInput!): User!

View File

@ -51,6 +51,14 @@ func (r *mutationResolver) Revoke(ctx context.Context, params model.OAuthRevokeI
return resolvers.RevokeResolver(ctx, params) return resolvers.RevokeResolver(ctx, params)
} }
func (r *mutationResolver) VerifyOtp(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) {
return resolvers.VerifyOtpResolver(ctx, params)
}
func (r *mutationResolver) ResendOtp(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) {
return resolvers.ResendOTPResolver(ctx, params)
}
func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) { func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) {
return resolvers.DeleteUserResolver(ctx, params) return resolvers.DeleteUserResolver(ctx, params)
} }

View File

@ -31,6 +31,9 @@ func InitMemStore() error {
constants.EnvKeyDisableLoginPage: false, constants.EnvKeyDisableLoginPage: false,
constants.EnvKeyDisableSignUp: false, constants.EnvKeyDisableSignUp: false,
constants.EnvKeyDisableStrongPassword: false, constants.EnvKeyDisableStrongPassword: false,
constants.EnvKeyIsEmailServiceEnabled: false,
constants.EnvKeyEnforceMultiFactorAuthentication: false,
constants.EnvKeyDisableMultiFactorAuthentication: false,
} }
requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv() requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv()

View File

@ -39,6 +39,7 @@ func (s *SessionStore) Set(key string, subKey, value string) {
func (s *SessionStore) RemoveAll(key string) { func (s *SessionStore) RemoveAll(key string) {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
delete(s.store, key) delete(s.store, key)
} }
@ -53,6 +54,9 @@ func (s *SessionStore) Remove(key, subKey string) {
// Get all the values for given key // Get all the values for given key
func (s *SessionStore) GetAll(key string) map[string]string { func (s *SessionStore) GetAll(key string) map[string]string {
s.mutex.Lock()
defer s.mutex.Unlock()
if _, ok := s.store[key]; !ok { if _, ok := s.store[key]; !ok {
s.store[key] = make(map[string]string) s.store[key] = make(map[string]string)
} }
@ -63,6 +67,7 @@ func (s *SessionStore) GetAll(key string) map[string]string {
func (s *SessionStore) RemoveByNamespace(namespace string) error { func (s *SessionStore) RemoveByNamespace(namespace string) error {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
for key := range s.store { for key := range s.store {
if strings.Contains(key, namespace+":") { if strings.Contains(key, namespace+":") {
delete(s.store, key) delete(s.store, key)

View File

@ -160,7 +160,7 @@ func (c *provider) GetEnvStore() (map[string]interface{}, error) {
return nil, err return nil, err
} }
for key, value := range data { for key, value := range data {
if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword { if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled || key == constants.EnvKeyEnforceMultiFactorAuthentication || key == constants.EnvKeyDisableMultiFactorAuthentication {
boolValue, err := strconv.ParseBool(value) boolValue, err := strconv.ParseBool(value)
if err != nil { if err != nil {
return res, err return res, err

View File

@ -170,6 +170,8 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
res.DisableLoginPage = store[constants.EnvKeyDisableLoginPage].(bool) res.DisableLoginPage = store[constants.EnvKeyDisableLoginPage].(bool)
res.DisableSignUp = store[constants.EnvKeyDisableSignUp].(bool) res.DisableSignUp = store[constants.EnvKeyDisableSignUp].(bool)
res.DisableStrongPassword = store[constants.EnvKeyDisableStrongPassword].(bool) res.DisableStrongPassword = store[constants.EnvKeyDisableStrongPassword].(bool)
res.EnforceMultiFactorAuthentication = store[constants.EnvKeyEnforceMultiFactorAuthentication].(bool)
res.DisableMultiFactorAuthentication = store[constants.EnvKeyDisableMultiFactorAuthentication].(bool)
return res, nil return res, nil
} }

View File

@ -16,6 +16,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
@ -35,13 +36,13 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput)
} }
// this feature is only allowed if email server is configured // this feature is only allowed if email server is configured
isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) EnvKeyIsEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil { if err != nil {
log.Debug("Error getting email verification disabled: ", err) log.Debug("Error getting email verification disabled: ", err)
isEmailVerificationDisabled = true EnvKeyIsEmailServiceEnabled = false
} }
if isEmailVerificationDisabled { if !EnvKeyIsEmailServiceEnabled {
log.Debug("Email server is not configured") log.Debug("Email server is not configured")
return nil, errors.New("email sending is disabled") return nil, errors.New("email sending is disabled")
} }
@ -136,6 +137,15 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput)
user.SignupMethods = constants.AuthRecipeMethodBasicAuth user.SignupMethods = constants.AuthRecipeMethodBasicAuth
verificationRequest.Identifier = constants.VerificationTypeForgotPassword verificationRequest.Identifier = constants.VerificationTypeForgotPassword
isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication)
if err != nil {
log.Debug("MFA service not enabled: ", err)
isMFAEnforced = false
}
if isMFAEnforced {
user.IsMultiFactorAuthEnabled = refs.NewBoolRef(true)
}
verifyEmailURL = appURL + "/setup-password" verifyEmailURL = appURL + "/setup-password"
} }

View File

@ -13,8 +13,10 @@ import (
"github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
@ -97,6 +99,42 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
scope = params.Scope scope = params.Scope
} }
isEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isEmailServiceEnabled {
log.Debug("Email service not enabled: ", err)
}
isMFADisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMultiFactorAuthentication)
if err != nil || !isEmailServiceEnabled {
log.Debug("MFA service not enabled: ", err)
}
// If email service is not enabled continue the process in any way
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && isEmailServiceEnabled && !isMFADisabled {
otp := utils.GenerateOTP()
otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{
Email: user.Email,
Otp: otp,
ExpiresAt: time.Now().Add(1 * time.Minute).Unix(),
})
if err != nil {
log.Debug("Failed to add otp: ", err)
return nil, err
}
go func() {
err := email.SendOtpMail(user.Email, otpData.Otp)
if err != nil {
log.Debug("Failed to send otp email: ", err)
}
}()
return &model.AuthResponse{
Message: "Please check the OTP in your inbox",
ShouldShowOtpScreen: refs.NewBoolRef(true),
}, nil
}
authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth) authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth)
if err != nil { if err != nil {
log.Debug("Failed to create auth token", err) log.Debug("Failed to create auth token", err)

View File

@ -107,6 +107,12 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) {
isSignUpDisabled = true isSignUpDisabled = true
} }
isMultiFactorAuthenticationEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMultiFactorAuthentication)
if err != nil {
log.Debug("Failed to get Disable Multi Factor Authentication from environment variable", err)
isSignUpDisabled = true
}
metaInfo := model.Meta{ metaInfo := model.Meta{
Version: constants.VERSION, Version: constants.VERSION,
ClientID: clientID, ClientID: clientID,
@ -120,6 +126,7 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) {
IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled, IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled,
IsSignUpEnabled: !isSignUpDisabled, IsSignUpEnabled: !isSignUpDisabled,
IsStrongPasswordEnabled: !isStrongPasswordDisabled, IsStrongPasswordEnabled: !isStrongPasswordDisabled,
IsMultiFactorAuthEnabled: !isMultiFactorAuthenticationEnabled,
} }
return &metaInfo, nil return &metaInfo, nil
} }

View File

@ -0,0 +1,96 @@
package resolvers
import (
"context"
"errors"
"fmt"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/utils"
)
// ResendOTPResolver is a resolver for resend otp mutation
func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) {
log := log.WithFields(log.Fields{
"email": params.Email,
})
params.Email = strings.ToLower(params.Email)
user, err := db.Provider.GetUserByEmail(ctx, params.Email)
if err != nil {
log.Debug("Failed to get user by email: ", err)
return nil, fmt.Errorf(`user with this email not found`)
}
if user.RevokedTimestamp != nil {
log.Debug("User access is revoked")
return nil, fmt.Errorf(`user access has been revoked`)
}
if user.EmailVerifiedAt == nil {
log.Debug("User email is not verified")
return nil, fmt.Errorf(`email not verified`)
}
if !refs.BoolValue(user.IsMultiFactorAuthEnabled) {
log.Debug("User multi factor authentication is not enabled")
return nil, fmt.Errorf(`multi factor authentication not enabled`)
}
isEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isEmailServiceEnabled {
log.Debug("Email service not enabled: ", err)
return nil, errors.New("email service not enabled")
}
isMFADisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMultiFactorAuthentication)
if err != nil || isMFADisabled {
log.Debug("MFA service not enabled: ", err)
return nil, errors.New("multi factor authentication is disabled for this instance")
}
// get otp by email
otpData, err := db.Provider.GetOTPByEmail(ctx, params.Email)
if err != nil {
log.Debug("Failed to get otp for given email: ", err)
return nil, err
}
if otpData == nil {
log.Debug("No otp found for given email: ", params.Email)
return &model.Response{
Message: "Failed to get for given email",
}, errors.New("failed to get otp for given email")
}
otp := utils.GenerateOTP()
otpData, err = db.Provider.UpsertOTP(ctx, &models.OTP{
Email: user.Email,
Otp: otp,
ExpiresAt: time.Now().Add(1 * time.Minute).Unix(),
})
if err != nil {
log.Debug("Error generating new otp: ", err)
return nil, err
}
go func() {
err := email.SendOtpMail(params.Email, otp)
if err != nil {
log.Debug("Error sending otp email: ", otp)
}
}()
return &model.Response{
Message: `OTP has been sent. Please check your inbox`,
}, nil
}

View File

@ -14,6 +14,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
@ -84,6 +85,16 @@ func ResetPasswordResolver(ctx context.Context, params model.ResetPasswordInput)
signupMethod := user.SignupMethods signupMethod := user.SignupMethods
if !strings.Contains(signupMethod, constants.AuthRecipeMethodBasicAuth) { if !strings.Contains(signupMethod, constants.AuthRecipeMethodBasicAuth) {
signupMethod = signupMethod + "," + constants.AuthRecipeMethodBasicAuth signupMethod = signupMethod + "," + constants.AuthRecipeMethodBasicAuth
isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication)
if err != nil {
log.Debug("MFA service not enabled: ", err)
isMFAEnforced = false
}
if isMFAEnforced {
user.IsMultiFactorAuthEnabled = refs.NewBoolRef(true)
}
} }
user.SignupMethods = signupMethod user.SignupMethods = signupMethod

View File

@ -17,6 +17,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
@ -157,6 +158,20 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
user.Picture = params.Picture user.Picture = params.Picture
} }
if params.IsMultiFactorAuthEnabled != nil {
user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled
}
isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication)
if err != nil {
log.Debug("MFA service not enabled: ", err)
isMFAEnforced = false
}
if isMFAEnforced {
user.IsMultiFactorAuthEnabled = refs.NewBoolRef(true)
}
user.SignupMethods = constants.AuthRecipeMethodBasicAuth user.SignupMethods = constants.AuthRecipeMethodBasicAuth
isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification)
if err != nil { if err != nil {

View File

@ -234,6 +234,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
// handle derivative cases like disabling email verification & magic login // handle derivative cases like disabling email verification & magic login
// in case SMTP is off but env is set to true // in case SMTP is off but env is set to true
if updatedData[constants.EnvKeySmtpHost] == "" || updatedData[constants.EnvKeySmtpUsername] == "" || updatedData[constants.EnvKeySmtpPassword] == "" || updatedData[constants.EnvKeySenderEmail] == "" && updatedData[constants.EnvKeySmtpPort] == "" { if updatedData[constants.EnvKeySmtpHost] == "" || updatedData[constants.EnvKeySmtpUsername] == "" || updatedData[constants.EnvKeySmtpPassword] == "" || updatedData[constants.EnvKeySenderEmail] == "" && updatedData[constants.EnvKeySmtpPort] == "" {
updatedData[constants.EnvKeyIsEmailServiceEnabled] = false
updatedData[constants.EnvKeyDisableMultiFactorAuthentication] = true
if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) { if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) {
updatedData[constants.EnvKeyDisableEmailVerification] = true updatedData[constants.EnvKeyDisableEmailVerification] = true
} }
@ -243,6 +245,16 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
} }
} }
if updatedData[constants.EnvKeySmtpHost] != "" || updatedData[constants.EnvKeySmtpUsername] != "" || updatedData[constants.EnvKeySmtpPassword] != "" || updatedData[constants.EnvKeySenderEmail] != "" && updatedData[constants.EnvKeySmtpPort] != "" {
updatedData[constants.EnvKeyIsEmailServiceEnabled] = true
}
if !currentData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && updatedData[constants.EnvKeyEnforceMultiFactorAuthentication].(bool) && !updatedData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) {
go db.Provider.UpdateUsers(ctx, map[string]interface{}{
"is_multi_factor_auth_enabled": true,
}, nil)
}
// check the roles change // check the roles change
if len(params.Roles) > 0 { if len(params.Roles) > 0 {
if len(params.DefaultRoles) > 0 { if len(params.DefaultRoles) > 0 {
@ -265,8 +277,6 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
} }
} }
go clearSessionIfRequired(currentData, updatedData)
// Update local store // Update local store
memorystore.Provider.UpdateEnvStore(updatedData) memorystore.Provider.UpdateEnvStore(updatedData)
jwk, err := crypto.GenerateJWKBasedOnEnv() jwk, err := crypto.GenerateJWKBasedOnEnv()
@ -320,6 +330,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
return res, err return res, err
} }
go clearSessionIfRequired(currentData, updatedData)
res = &model.Response{ res = &model.Response{
Message: "configurations updated successfully", Message: "configurations updated successfully",
} }

View File

@ -2,6 +2,7 @@ package resolvers
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -46,7 +47,7 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
} }
// validate if all params are not empty // validate if all params are not empty
if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.OldPassword == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil && params.NewPassword == nil && params.ConfirmNewPassword == nil { if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.OldPassword == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil && params.NewPassword == nil && params.ConfirmNewPassword == nil && params.IsMultiFactorAuthEnabled == nil {
log.Debug("All params are empty") log.Debug("All params are empty")
return res, fmt.Errorf("please enter at least one param to update") return res, fmt.Errorf("please enter at least one param to update")
} }
@ -94,6 +95,29 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
user.Picture = params.Picture user.Picture = params.Picture
} }
if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) {
if refs.BoolValue(params.IsMultiFactorAuthEnabled) {
isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isEnvServiceEnabled {
log.Debug("Email service not enabled:")
return nil, errors.New("email service not enabled, so cannot enable multi factor authentication")
}
}
isMFAEnforced, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyEnforceMultiFactorAuthentication)
if err != nil {
log.Debug("MFA service not enabled: ", err)
isMFAEnforced = false
}
if isMFAEnforced && !refs.BoolValue(params.IsMultiFactorAuthEnabled) {
log.Debug("Cannot disable mfa service as it is enforced:")
return nil, errors.New("cannot disable multi factor authentication as it is enforced by organization")
}
user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled
}
isPasswordChanging := false isPasswordChanging := false
if params.NewPassword != nil && params.ConfirmNewPassword == nil { if params.NewPassword != nil && params.ConfirmNewPassword == nil {
isPasswordChanging = true isPasswordChanging = true

View File

@ -2,6 +2,7 @@ package resolvers
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -15,6 +16,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/parsers"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/authorizerdev/authorizer/server/validators" "github.com/authorizerdev/authorizer/server/validators"
@ -45,7 +47,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
"user_id": params.ID, "user_id": params.ID,
}) })
if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil && params.Roles == nil { if params.GivenName == nil && params.FamilyName == nil && params.Picture == nil && params.MiddleName == nil && params.Nickname == nil && params.Email == nil && params.Birthdate == nil && params.Gender == nil && params.PhoneNumber == nil && params.Roles == nil && params.IsMultiFactorAuthEnabled == nil {
log.Debug("No params to update") log.Debug("No params to update")
return res, fmt.Errorf("please enter atleast one param to update") return res, fmt.Errorf("please enter atleast one param to update")
} }
@ -56,38 +58,49 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
return res, fmt.Errorf(`User not found`) return res, fmt.Errorf(`User not found`)
} }
if params.GivenName != nil && user.GivenName != params.GivenName { if params.GivenName != nil && refs.StringValue(user.GivenName) != refs.StringValue(params.GivenName) {
user.GivenName = params.GivenName user.GivenName = params.GivenName
} }
if params.FamilyName != nil && user.FamilyName != params.FamilyName { if params.FamilyName != nil && refs.StringValue(user.FamilyName) != refs.StringValue(params.FamilyName) {
user.FamilyName = params.FamilyName user.FamilyName = params.FamilyName
} }
if params.MiddleName != nil && user.MiddleName != params.MiddleName { if params.MiddleName != nil && refs.StringValue(user.MiddleName) != refs.StringValue(params.MiddleName) {
user.MiddleName = params.MiddleName user.MiddleName = params.MiddleName
} }
if params.Nickname != nil && user.Nickname != params.Nickname { if params.Nickname != nil && refs.StringValue(user.Nickname) != refs.StringValue(params.Nickname) {
user.Nickname = params.Nickname user.Nickname = params.Nickname
} }
if params.Birthdate != nil && user.Birthdate != params.Birthdate { if params.Birthdate != nil && refs.StringValue(user.Birthdate) != refs.StringValue(params.Birthdate) {
user.Birthdate = params.Birthdate user.Birthdate = params.Birthdate
} }
if params.Gender != nil && user.Gender != params.Gender { if params.Gender != nil && refs.StringValue(user.Gender) != refs.StringValue(params.Gender) {
user.Gender = params.Gender user.Gender = params.Gender
} }
if params.PhoneNumber != nil && user.PhoneNumber != params.PhoneNumber { if params.PhoneNumber != nil && refs.StringValue(user.PhoneNumber) != refs.StringValue(params.PhoneNumber) {
user.PhoneNumber = params.PhoneNumber user.PhoneNumber = params.PhoneNumber
} }
if params.Picture != nil && user.Picture != params.Picture { if params.Picture != nil && refs.StringValue(user.Picture) != refs.StringValue(params.Picture) {
user.Picture = params.Picture user.Picture = params.Picture
} }
if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) {
user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled
if refs.BoolValue(params.IsMultiFactorAuthEnabled) {
isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil || !isEnvServiceEnabled {
log.Debug("Email service not enabled:")
return nil, errors.New("email service not enabled, so cannot enable multi factor authentication")
}
}
}
if params.EmailVerified != nil { if params.EmailVerified != nil {
if *params.EmailVerified { if *params.EmailVerified {
now := time.Now().Unix() now := time.Now().Unix()

View File

@ -0,0 +1,104 @@
package resolvers
import (
"context"
"fmt"
"strings"
"time"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
log "github.com/sirupsen/logrus"
)
// VerifyOtpResolver resolver for verify otp mutation
func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*model.AuthResponse, error) {
var res *model.AuthResponse
gc, err := utils.GinContextFromContext(ctx)
if err != nil {
log.Debug("Failed to get GinContext: ", err)
return res, err
}
otp, err := db.Provider.GetOTPByEmail(ctx, params.Email)
if err != nil {
log.Debug("Failed to get otp request by email: ", err)
return res, fmt.Errorf(`invalid email: %s`, err.Error())
}
if params.Otp != otp.Otp {
log.Debug("Failed to verify otp request: Incorrect value")
return res, fmt.Errorf(`invalid otp`)
}
expiresIn := otp.ExpiresAt - time.Now().Unix()
if expiresIn < 0 {
log.Debug("Failed to verify otp request: Timeout")
return res, fmt.Errorf("otp expired")
}
user, err := db.Provider.GetUserByEmail(ctx, params.Email)
if err != nil {
log.Debug("Failed to get user by email: ", err)
return res, err
}
isSignUp := user.EmailVerifiedAt == nil
// TODO - Add Login method in DB when we introduce OTP for social media login
loginMethod := constants.AuthRecipeMethodBasicAuth
roles := strings.Split(user.Roles, ",")
scope := []string{"openid", "email", "profile"}
authToken, err := token.CreateAuthToken(gc, user, roles, scope, loginMethod)
if err != nil {
log.Debug("Failed to create auth token: ", err)
return res, err
}
go func() {
db.Provider.DeleteOTP(gc, otp)
if isSignUp {
utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user)
} else {
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, loginMethod, user)
}
db.Provider.AddSession(ctx, models.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(gc.Request),
IP: utils.GetIP(gc.Request),
})
}()
authTokenExpiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
if authTokenExpiresIn <= 0 {
authTokenExpiresIn = 1
}
res = &model.AuthResponse{
Message: `OTP verified successfully.`,
AccessToken: &authToken.AccessToken.Token,
IDToken: &authToken.IDToken.Token,
ExpiresIn: &authTokenExpiresIn,
User: user.AsAPIUser(),
}
sessionKey := loginMethod + ":" + user.ID
cookie.SetSession(gc, authToken.FingerPrintHash)
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+authToken.FingerPrint, authToken.FingerPrintHash)
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token)
if authToken.RefreshToken != nil {
res.RefreshToken = &authToken.RefreshToken.Token
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token)
}
return res, nil
}

View File

@ -0,0 +1,99 @@
package test
import (
"context"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func resendOTPTest(t *testing.T, s TestSetup) {
t.Helper()
t.Run(`should resend otp`, func(t *testing.T) {
req, ctx := createContext(s)
email := "resend_otp." + s.TestInfo.Email
res, err := resolvers.SignupResolver(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
assert.NoError(t, err)
assert.NotNil(t, res)
// Login should fail as email is not verified
loginRes, err := resolvers.LoginResolver(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
})
assert.Error(t, err)
assert.Nil(t, loginRes)
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup)
assert.Nil(t, err)
assert.Equal(t, email, verificationRequest.Email)
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
Token: verificationRequest.Token,
})
assert.Nil(t, err)
assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty")
// Using access token update profile
s.GinContext.Request.Header.Set("Authorization", "Bearer "+refs.StringValue(verifyRes.AccessToken))
ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext)
_, err = resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{
IsMultiFactorAuthEnabled: refs.NewBoolRef(true),
})
// Resend otp should return error as no initial opt is being sent
resendOtpRes, err := resolvers.ResendOTPResolver(ctx, model.ResendOTPRequest{
Email: email,
})
assert.Error(t, err)
assert.Nil(t, resendOtpRes)
// Login should not return error but access token should be empty as otp should have been sent
loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
})
assert.NoError(t, err)
assert.NotNil(t, loginRes)
assert.Nil(t, loginRes.AccessToken)
// Get otp from db
otp, err := db.Provider.GetOTPByEmail(ctx, email)
assert.NoError(t, err)
assert.NotEmpty(t, otp.Otp)
// resend otp
resendOtpRes, err = resolvers.ResendOTPResolver(ctx, model.ResendOTPRequest{
Email: email,
})
assert.NoError(t, err)
assert.NotEmpty(t, resendOtpRes.Message)
newOtp, err := db.Provider.GetOTPByEmail(ctx, email)
assert.NoError(t, err)
assert.NotEmpty(t, newOtp.Otp)
assert.NotEqual(t, otp.Otp, newOtp)
// Should return error for older otp
verifyOtpRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{
Email: email,
Otp: otp.Otp,
})
assert.Error(t, err)
verifyOtpRes, err = resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{
Email: email,
Otp: newOtp.Otp,
})
assert.NoError(t, err)
assert.NotEqual(t, verifyOtpRes.AccessToken, "", "access token should not be empty")
cleanData(email)
})
}

View File

@ -33,7 +33,7 @@ func TestResolvers(t *testing.T) {
if utils.StringSliceContains(testDBs, constants.DbTypeSqlite) && len(testDBs) == 1 { if utils.StringSliceContains(testDBs, constants.DbTypeSqlite) && len(testDBs) == 1 {
// do nothing // do nothing
} else { } else {
t.Log("waiting for docker containers to spun up") t.Log("waiting for docker containers to start...")
// wait for docker containers to spun up // wait for docker containers to spun up
time.Sleep(30 * time.Second) time.Sleep(30 * time.Second)
} }
@ -114,7 +114,10 @@ func TestResolvers(t *testing.T) {
metaTests(t, s) metaTests(t, s)
inviteUserTest(t, s) inviteUserTest(t, s)
validateJwtTokenTest(t, s) validateJwtTokenTest(t, s)
verifyOTPTest(t, s)
resendOTPTest(t, s)
updateAllUsersTest(t, s)
webhookLogsTest(t, s) // get logs after above resolver tests are done webhookLogsTest(t, s) // get logs after above resolver tests are done
deleteWebhookTest(t, s) // delete webhooks (admin resolver) deleteWebhookTest(t, s) // delete webhooks (admin resolver)
}) })

View File

@ -57,6 +57,11 @@ func cleanData(email string) {
err = db.Provider.DeleteVerificationRequest(ctx, verificationRequest) err = db.Provider.DeleteVerificationRequest(ctx, verificationRequest)
} }
otp, err := db.Provider.GetOTPByEmail(ctx, email)
if err == nil {
err = db.Provider.DeleteOTP(ctx, otp)
}
dbUser, err := db.Provider.GetUserByEmail(ctx, email) dbUser, err := db.Provider.GetUserByEmail(ctx, email)
if err == nil { if err == nil {
db.Provider.DeleteUser(ctx, dbUser) db.Provider.DeleteUser(ctx, dbUser)

View File

@ -0,0 +1,67 @@
package test
import (
"fmt"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/stretchr/testify/assert"
)
func updateAllUsersTest(t *testing.T, s TestSetup) {
t.Helper()
t.Run("Should update all users", func(t *testing.T) {
_, ctx := createContext(s)
users := []models.User{}
for i := 0; i < 10; i++ {
user := models.User{
Email: fmt.Sprintf("update_all_user_%d_%s", i, s.TestInfo.Email),
SignupMethods: constants.AuthRecipeMethodBasicAuth,
Roles: "user",
}
users = append(users, user)
u, err := db.Provider.AddUser(ctx, user)
assert.NoError(t, err)
assert.NotNil(t, u)
}
err := db.Provider.UpdateUsers(ctx, map[string]interface{}{
"is_multi_factor_auth_enabled": true,
}, nil)
assert.NoError(t, err)
listUsers, err := db.Provider.ListUsers(ctx, model.Pagination{
Limit: 20,
Offset: 0,
})
assert.NoError(t, err)
for _, u := range listUsers.Users {
assert.True(t, refs.BoolValue(u.IsMultiFactorAuthEnabled))
}
// // update few users
updateIds := []string{listUsers.Users[0].ID, listUsers.Users[1].ID}
err = db.Provider.UpdateUsers(ctx, map[string]interface{}{
"is_multi_factor_auth_enabled": false,
}, updateIds)
assert.NoError(t, err)
listUsers, err = db.Provider.ListUsers(ctx, model.Pagination{
Limit: 20,
Offset: 0,
})
for _, u := range listUsers.Users {
if utils.StringSliceContains(updateIds, u.ID) {
assert.False(t, refs.BoolValue(u.IsMultiFactorAuthEnabled))
} else {
assert.True(t, refs.BoolValue(u.IsMultiFactorAuthEnabled))
}
}
})
}

View File

@ -0,0 +1,75 @@
package test
import (
"context"
"testing"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/stretchr/testify/assert"
)
func verifyOTPTest(t *testing.T, s TestSetup) {
t.Helper()
t.Run(`should verify otp`, func(t *testing.T) {
req, ctx := createContext(s)
email := "verify_otp." + s.TestInfo.Email
res, err := resolvers.SignupResolver(ctx, model.SignUpInput{
Email: email,
Password: s.TestInfo.Password,
ConfirmPassword: s.TestInfo.Password,
})
assert.NoError(t, err)
assert.NotNil(t, res)
// Login should fail as email is not verified
loginRes, err := resolvers.LoginResolver(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
})
assert.Error(t, err)
assert.Nil(t, loginRes)
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(ctx, email, constants.VerificationTypeBasicAuthSignup)
assert.Nil(t, err)
assert.Equal(t, email, verificationRequest.Email)
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
Token: verificationRequest.Token,
})
assert.Nil(t, err)
assert.NotEqual(t, verifyRes.AccessToken, "", "access token should not be empty")
// Using access token update profile
s.GinContext.Request.Header.Set("Authorization", "Bearer "+refs.StringValue(verifyRes.AccessToken))
ctx = context.WithValue(req.Context(), "GinContextKey", s.GinContext)
updateProfileRes, err := resolvers.UpdateProfileResolver(ctx, model.UpdateProfileInput{
IsMultiFactorAuthEnabled: refs.NewBoolRef(true),
})
assert.NoError(t, err)
assert.NotEmpty(t, updateProfileRes.Message)
// Login should not return error but access token should be empty as otp should have been sent
loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
})
assert.NoError(t, err)
assert.NotNil(t, loginRes)
assert.Nil(t, loginRes.AccessToken)
// Get otp from db
otp, err := db.Provider.GetOTPByEmail(ctx, email)
assert.NoError(t, err)
assert.NotEmpty(t, otp.Otp)
verifyOtpRes, err := resolvers.VerifyOtpResolver(ctx, model.VerifyOTPRequest{
Email: email,
Otp: otp.Otp,
})
assert.Nil(t, err)
assert.NotEqual(t, verifyOtpRes.AccessToken, "", "access token should not be empty")
cleanData(email)
})
}

View File

@ -0,0 +1,25 @@
package utils
import (
"math/rand"
"time"
)
// GenerateOTP to generate random 6 digit otp
func GenerateOTP() string {
code := ""
codeLength := 6
charSet := "ABCDEFGHJKLMNPQRSTUVWXYZ123456789"
charSetLength := int32(len(charSet))
for i := 0; i < codeLength; i++ {
index := randomNumber(0, charSetLength)
code += string(charSet[index])
}
return code
}
func randomNumber(min, max int32) int32 {
rand.Seed(time.Now().UnixNano())
return min + int32(rand.Intn(int(max-min)))
}

View File

@ -7,7 +7,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// TODO renamae GinContextKey -> GinContext // TODO re-name GinContextKey -> GinContext
// GinContext to get gin context from context // GinContext to get gin context from context
func GinContextFromContext(ctx context.Context) (*gin.Context, error) { func GinContextFromContext(ctx context.Context) (*gin.Context, error) {

View File

@ -11,6 +11,7 @@ import (
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models" "github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/refs" "github.com/authorizerdev/authorizer/server/refs"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -52,6 +53,22 @@ func RegisterEvent(ctx context.Context, eventName string, authRecipe string, use
return err return err
} }
// dont trigger webhook call in case of test
envKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyEnv)
if err != nil {
return err
}
if envKey == constants.TestEnv {
db.Provider.AddWebhookLog(ctx, models.WebhookLog{
HttpStatus: 200,
Request: string(requestBody),
Response: string(`{"message": "test"}`),
WebhookID: webhook.ID,
})
return nil
}
requestBytesBuffer := bytes.NewBuffer(requestBody) requestBytesBuffer := bytes.NewBuffer(requestBody)
req, err := http.NewRequest("POST", refs.StringValue(webhook.Endpoint), requestBytesBuffer) req, err := http.NewRequest("POST", refs.StringValue(webhook.Endpoint), requestBytesBuffer)
if err != nil { if err != nil {