This commit is contained in:
Anand Kumar Panigrahi 2023-10-02 01:37:50 -06:00 committed by GitHub
commit 0860c33920
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1576 additions and 25 deletions

View File

@ -4,6 +4,7 @@ import InputField from '../InputField';
import { SwitchInputType } from '../../constants';
const Features = ({ variables, setVariables }: any) => {
// window.alert(variables)
return (
<div>
{' '}
@ -24,6 +25,8 @@ const Features = ({ variables, setVariables }: any) => {
/>
</Flex>
</Flex>
<Flex>
<Flex w="100%" justifyContent="start" alignItems="center">
<Text fontSize="sm">Email Verification:</Text>
@ -97,6 +100,7 @@ const Features = ({ variables, setVariables }: any) => {
also ignore the user MFA setting.
</Text>
</Flex>
<Flex justifyContent="start" mb={3}>
<InputField
variables={variables}
@ -106,6 +110,46 @@ const Features = ({ variables, setVariables }: any) => {
/>
</Flex>
</Flex>
{
!variables.DISABLE_MULTI_FACTOR_AUTHENTICATION &&
<Flex alignItems="center">
<Flex w="100%" alignItems="baseline" flexDir="column">
<Text fontSize="sm">TOTP:</Text>
<Text fontSize="x-small">
Note: to enable totp mfa
</Text>
</Flex>
<Flex justifyContent="start" mb={3}>
<InputField
variables={variables}
setVariables={setVariables}
inputType={SwitchInputType.DISABLE_TOTP_LOGIN}
hasReversedValue
/>
</Flex>
</Flex>
}
{!variables.DISABLE_MULTI_FACTOR_AUTHENTICATION &&
<Flex alignItems="center">
<Flex w="100%" alignItems="baseline" flexDir="column">
<Text fontSize="sm">EMAIL OTP:</Text>
<Text fontSize="x-small">
Note: to enable email otp mfa
</Text>
</Flex>
<Flex justifyContent="start" mb={3}>
<InputField
variables={variables}
setVariables={setVariables}
inputType={SwitchInputType.DISABLE_MAIL_OTP_LOGIN}
hasReversedValue
/>
</Flex>
</Flex>}
<Flex alignItems="center">
<Flex w="100%" alignItems="baseline" flexDir="column">
<Text fontSize="sm">

View File

@ -85,6 +85,8 @@ export const SwitchInputType = {
DISABLE_MULTI_FACTOR_AUTHENTICATION: 'DISABLE_MULTI_FACTOR_AUTHENTICATION',
ENFORCE_MULTI_FACTOR_AUTHENTICATION: 'ENFORCE_MULTI_FACTOR_AUTHENTICATION',
DISABLE_PLAYGROUND: 'DISABLE_PLAYGROUND',
DISABLE_TOTP_LOGIN: 'DISABLE_TOTP_LOGIN',
DISABLE_MAIL_OTP_LOGIN: 'DISABLE_MAIL_OTP_LOGIN',
};
export const DateInputType = {
@ -169,6 +171,8 @@ export interface envVarTypes {
DEFAULT_AUTHORIZE_RESPONSE_TYPE: string;
DEFAULT_AUTHORIZE_RESPONSE_MODE: string;
DISABLE_PLAYGROUND: boolean;
DISABLE_TOTP_LOGIN: boolean;
DISABLE_MAIL_OTP_LOGIN: boolean;
}
export const envSubViews = {

View File

@ -74,6 +74,8 @@ export const EnvVariablesQuery = `
DEFAULT_AUTHORIZE_RESPONSE_TYPE
DEFAULT_AUTHORIZE_RESPONSE_MODE
DISABLE_PLAYGROUND
DISABLE_TOTP_LOGIN
DISABLE_MAIL_OTP_LOGIN
}
}
`;

View File

@ -94,6 +94,8 @@ const Environment = () => {
DEFAULT_AUTHORIZE_RESPONSE_TYPE: '',
DEFAULT_AUTHORIZE_RESPONSE_MODE: '',
DISABLE_PLAYGROUND: false,
DISABLE_TOTP_LOGIN: false,
DISABLE_MAIL_OTP_LOGIN: true,
});
const [fieldVisibility, setFieldVisibility] = React.useState<

View File

@ -160,6 +160,12 @@ const (
// 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"
// EnvKeyDisableTOTPLogin is key for env variable DISABLE_TOTP_LOGIN
// this variable is used to completely disable totp verification
EnvKeyDisableTOTPLogin = "DISABLE_TOTP_LOGIN"
// EnvKeyDisableMailOTPLogin is key for env variable DISABLE_MAIL_OTP_LOGIN
// this variable is used to completely disable totp verification
EnvKeyDisableMailOTPLogin = "DISABLE_MAIL_OTP_LOGIN"
// EnvKeyDisablePhoneVerification is key for env variable DISABLE_PHONE_VERIFICATION
// this variable is used to disable phone verification
EnvKeyDisablePhoneVerification = "DISABLE_PHONE_VERIFICATION"

View File

@ -3,7 +3,9 @@ package crypto
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
)
@ -116,3 +118,24 @@ func AsRSAStr(privateKey *rsa.PrivateKey, publickKey *rsa.PublicKey) (string, st
return privParsedPem, pubParsedPem, nil
}
func EncryptRSA(message string, key rsa.PublicKey) (string, error) {
label := []byte("OAEP Encrypted")
rng := rand.Reader
ciphertext, err := rsa.EncryptOAEP(sha256.New(), rng, &key, []byte(message), label)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func DecryptRSA(cipherText string, privateKey rsa.PrivateKey) (string, error) {
ct, _ := base64.StdEncoding.DecodeString(cipherText)
label := []byte("OAEP Encrypted")
rng := rand.Reader
plaintext, err := rsa.DecryptOAEP(sha256.New(), rng, &privateKey, ct, label)
if err != nil {
return "", err
}
return string(plaintext), nil
}

View File

@ -34,6 +34,8 @@ type User struct {
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at" dynamo:"updated_at"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at" dynamo:"created_at"`
AppData *string `json:"app_data" bson:"app_data" cql:"app_data" dynamo:"app_data"`
TotpSecret *string `json:"totp_secret" bson:"totp_secret" cql:"totp_secret" dynamo:"totp_secret"`
TotpVerified bool `json:"totp_verified" bson:"totp_verified" cql:"totp_verified" dynamo:"totp_verified"`
}
func (user *User) AsAPIUser() *model.User {

View File

@ -0,0 +1,70 @@
package arangodb
import (
"bytes"
"context"
"fmt"
"image/png"
"time"
"github.com/pquerna/otp/totp"
"github.com/authorizerdev/authorizer/server/crypto"
)
func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) {
var buf bytes.Buffer
//get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("error while getting user details")
}
// generate totp, TOTP hash is valid for 30 seconds
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "authorizer",
AccountName: user.Email,
})
if err != nil {
return nil, fmt.Errorf("error while genrating totp")
}
// get secret for user
secret := key.Secret()
//generating image for key and encoding to base64 for displaying in frontend
img, err := key.Image(200, 200)
if err != nil {
return nil, fmt.Errorf("error while creating qr image for totp")
}
png.Encode(&buf, img)
encodedText := crypto.EncryptB64(buf.String())
// update user totp secret in db
user.UpdatedAt = time.Now().Unix()
user.TotpSecret = &secret
_, err = p.UpdateUser(ctx, user)
if err != nil {
return nil, fmt.Errorf("error while updating user's totp secret")
}
return &encodedText, nil
}
func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) {
// get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return false, fmt.Errorf("error while getting user details")
}
status := totp.Validate(passcode, *user.TotpSecret)
if !user.TotpVerified {
if status {
user.TotpVerified = true
p.UpdateUser(ctx, user)
return status, nil
}
return status, nil
}
return status, nil
}

View File

@ -267,6 +267,14 @@ func NewProvider() (*provider, error) {
if err != nil {
return nil, err
}
// add totp_secret and totp_verified on users table
totpTableAlterQuery := fmt.Sprintf(`ALTER TABLE %s.%s ADD (totp_verified boolean, totp_secret text, app_data text)`, KeySpace, models.Collections.User)
err = session.Query(totpTableAlterQuery).Exec()
if err != nil {
log.Debug("Failed to alter table as column exists: ", err)
// return nil, err
}
return &provider{
db: session,
}, err

View File

@ -0,0 +1,70 @@
package cassandradb
import (
"bytes"
"context"
"fmt"
"image/png"
"time"
"github.com/pquerna/otp/totp"
"github.com/authorizerdev/authorizer/server/crypto"
)
func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) {
var buf bytes.Buffer
//get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("error while getting user details")
}
// generate totp, TOTP hash is valid for 30 seconds
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "authorizer",
AccountName: user.Email,
})
if err != nil {
return nil, fmt.Errorf("error while genrating totp")
}
// get secret for user
secret := key.Secret()
//generating image for key and encoding to base64 for displaying in frontend
img, err := key.Image(200, 200)
if err != nil {
return nil, fmt.Errorf("error while creating qr image for totp")
}
png.Encode(&buf, img)
encodedText := crypto.EncryptB64(buf.String())
// update user totp secret in db
user.UpdatedAt = time.Now().Unix()
user.TotpSecret = &secret
_, err = p.UpdateUser(ctx, user)
if err != nil {
return nil, fmt.Errorf("error while updating user's totp secret")
}
return &encodedText, nil
}
func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) {
// get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return false, fmt.Errorf("error while getting user details")
}
status := totp.Validate(passcode, *user.TotpSecret)
if !user.TotpVerified {
if status {
user.TotpVerified = true
p.UpdateUser(ctx, user)
return status, nil
}
return status, nil
}
return status, nil
}

View File

@ -39,6 +39,7 @@ func (p *provider) AddUser(ctx context.Context, user *models.User) (*models.User
user.CreatedAt = time.Now().Unix()
user.UpdatedAt = time.Now().Unix()
user.TotpVerified = false
bytes, err := json.Marshal(user)
if err != nil {
@ -177,13 +178,19 @@ func (p *provider) ListUsers(ctx context.Context, pagination *model.Pagination)
// there is no offset in cassandra
// so we fetch till limit + offset
// 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, is_multi_factor_auth_enabled, 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, totp_verified FROM %s LIMIT %d", KeySpace+"."+models.Collections.User,
pagination.Limit+pagination.Offset)
scanner := p.db.Query(query).Iter().Scanner()
counter := int64(0)
for scanner.Next() {
if counter >= pagination.Offset {
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.IsMultiFactorAuthEnabled, &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, &user.TotpVerified)
if err != nil {
return nil, err
}
@ -200,8 +207,8 @@ func (p *provider) ListUsers(ctx context.Context, pagination *model.Pagination)
// GetUserByEmail to get user information from database using email address
func (p *provider) GetUserByEmail(ctx context.Context, email string) (*models.User, error) {
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, 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.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
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, totp_secret, totp_verified, app_data 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.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt, &user.TotpSecret, &user.TotpVerified, &user.AppData)
if err != nil {
return nil, err
}
@ -211,8 +218,8 @@ func (p *provider) GetUserByEmail(ctx context.Context, email string) (*models.Us
// GetUserByID to get user information from database using user ID
func (p *provider) GetUserByID(ctx context.Context, id string) (*models.User, error) {
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, 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.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
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, totp_secret, totp_verified, app_data 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.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt, &user.TotpSecret, &user.TotpVerified, &user.AppData)
if err != nil {
return nil, err
}

View File

@ -0,0 +1,70 @@
package couchbase
import (
"bytes"
"context"
"fmt"
"image/png"
"time"
"github.com/pquerna/otp/totp"
"github.com/authorizerdev/authorizer/server/crypto"
)
func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) {
var buf bytes.Buffer
//get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("error while getting user details")
}
// generate totp, TOTP hash is valid for 30 seconds
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "authorizer",
AccountName: user.Email,
})
if err != nil {
return nil, fmt.Errorf("error while genrating totp")
}
// get secret for user
secret := key.Secret()
//generating image for key and encoding to base64 for displaying in frontend
img, err := key.Image(200, 200)
if err != nil {
return nil, fmt.Errorf("error while creating qr image for totp")
}
png.Encode(&buf, img)
encodedText := crypto.EncryptB64(buf.String())
// update user totp secret in db
user.UpdatedAt = time.Now().Unix()
user.TotpSecret = &secret
_, err = p.UpdateUser(ctx, user)
if err != nil {
return nil, fmt.Errorf("error while updating user's totp secret")
}
return &encodedText, nil
}
func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) {
// get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return false, fmt.Errorf("error while getting user details")
}
status := totp.Validate(passcode, *user.TotpSecret)
if !user.TotpVerified {
if status {
user.TotpVerified = true
p.UpdateUser(ctx, user)
return status, nil
}
return status, nil
}
return status, nil
}

View File

@ -103,7 +103,7 @@ func (p *provider) ListUsers(ctx context.Context, pagination *model.Pagination)
// GetUserByEmail to get user information from database using email address
func (p *provider) GetUserByEmail(ctx context.Context, email string) (*models.User, error) {
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, is_multi_factor_auth_enabled, created_at, updated_at FROM %s.%s WHERE email = $1 LIMIT 1", p.scopeName, models.Collections.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, is_multi_factor_auth_enabled, created_at, updated_at, totp_secret, totp_verified FROM %s.%s WHERE email = $1 LIMIT 1", p.scopeName, models.Collections.User)
q, err := p.db.Query(query, &gocb.QueryOptions{
ScanConsistency: gocb.QueryScanConsistencyRequestPlus,
Context: ctx,
@ -122,7 +122,7 @@ func (p *provider) GetUserByEmail(ctx context.Context, email string) (*models.Us
// GetUserByID to get user information from database using user ID
func (p *provider) GetUserByID(ctx context.Context, id string) (*models.User, error) {
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, is_multi_factor_auth_enabled, created_at, updated_at FROM %s.%s WHERE _id = $1 LIMIT 1", p.scopeName, models.Collections.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, is_multi_factor_auth_enabled, created_at, updated_at, totp_secret, totp_verified FROM %s.%s WHERE _id = $1 LIMIT 1", p.scopeName, models.Collections.User)
q, err := p.db.Query(query, &gocb.QueryOptions{
ScanConsistency: gocb.QueryScanConsistencyRequestPlus,
Context: ctx,

View File

@ -0,0 +1,70 @@
package dynamodb
import (
"bytes"
"context"
"fmt"
"image/png"
"time"
"github.com/pquerna/otp/totp"
"github.com/authorizerdev/authorizer/server/crypto"
)
func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) {
var buf bytes.Buffer
//get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("error while getting user details")
}
// generate totp, TOTP hash is valid for 30 seconds
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "authorizer",
AccountName: user.Email,
})
if err != nil {
return nil, fmt.Errorf("error while genrating totp")
}
// get secret for user
secret := key.Secret()
//generating image for key and encoding to base64 for displaying in frontend
img, err := key.Image(200, 200)
if err != nil {
return nil, fmt.Errorf("error while creating qr image for totp")
}
png.Encode(&buf, img)
encodedText := crypto.EncryptB64(buf.String())
// update user totp secret in db
user.UpdatedAt = time.Now().Unix()
user.TotpSecret = &secret
_, err = p.UpdateUser(ctx, user)
if err != nil {
return nil, fmt.Errorf("error while updating user's totp secret")
}
return &encodedText, nil
}
func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) {
// get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return false, fmt.Errorf("error while getting user details")
}
status := totp.Validate(passcode, *user.TotpSecret)
if !user.TotpVerified {
if status {
user.TotpVerified = true
p.UpdateUser(ctx, user)
return status, nil
}
return status, nil
}
return status, nil
}

View File

@ -0,0 +1,70 @@
package mongodb
import (
"bytes"
"context"
"fmt"
"image/png"
"time"
"github.com/pquerna/otp/totp"
"github.com/authorizerdev/authorizer/server/crypto"
)
func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) {
var buf bytes.Buffer
//get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("error while getting user details")
}
// generate totp, TOTP hash is valid for 30 seconds
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "authorizer",
AccountName: user.Email,
})
if err != nil {
return nil, fmt.Errorf("error while genrating totp")
}
// get secret for user
secret := key.Secret()
//generating image for key and encoding to base64 for displaying in frontend
img, err := key.Image(200, 200)
if err != nil {
return nil, fmt.Errorf("error while creating qr image for totp")
}
png.Encode(&buf, img)
encodedText := crypto.EncryptB64(buf.String())
// update user totp secret in db
user.UpdatedAt = time.Now().Unix()
user.TotpSecret = &secret
_, err = p.UpdateUser(ctx, user)
if err != nil {
return nil, fmt.Errorf("error while updating user's totp secret")
}
return &encodedText, nil
}
func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) {
// get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return false, fmt.Errorf("error while getting user details")
}
status := totp.Validate(passcode, *user.TotpSecret)
if !user.TotpVerified {
if status {
user.TotpVerified = true
p.UpdateUser(ctx, user)
return status, nil
}
return status, nil
}
return status, nil
}

View File

@ -0,0 +1,70 @@
package provider_template
import (
"bytes"
"context"
"fmt"
"image/png"
"time"
"github.com/pquerna/otp/totp"
"github.com/authorizerdev/authorizer/server/crypto"
)
func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) {
var buf bytes.Buffer
//get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("error while getting user details")
}
// generate totp, TOTP hash is valid for 30 seconds
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "authorizer",
AccountName: user.Email,
})
if err != nil {
return nil, fmt.Errorf("error while genrating totp")
}
// get secret for user
secret := key.Secret()
//generating image for key and encoding to base64 for displaying in frontend
img, err := key.Image(200, 200)
if err != nil {
return nil, fmt.Errorf("error while creating qr image for totp")
}
png.Encode(&buf, img)
encodedText := crypto.EncryptB64(buf.String())
// update user totp secret in db
user.UpdatedAt = time.Now().Unix()
user.TotpSecret = &secret
_, err = p.UpdateUser(ctx, user)
if err != nil {
return nil, fmt.Errorf("error while updating user's totp secret")
}
return &encodedText, nil
}
func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) {
// get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return false, fmt.Errorf("error while getting user details")
}
status := totp.Validate(passcode, *user.TotpSecret)
if !user.TotpVerified {
if status {
user.TotpVerified = true
p.UpdateUser(ctx, user)
return status, nil
}
return status, nil
}
return status, nil
}

View File

@ -2,7 +2,6 @@ package providers
import (
"context"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/graph/model"
)
@ -88,4 +87,9 @@ type Provider interface {
GetOTPByPhoneNumber(ctx context.Context, phoneNumber string) (*models.OTP, error)
// DeleteOTP to delete otp
DeleteOTP(ctx context.Context, otp *models.OTP) error
// GenerateTotp to generate totp, store secret into db and returns base64 of QR code image
GenerateTotp(ctx context.Context, id string) (*string, error)
// ValidatePasscode validate user passcode with secret stored in our db
ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error)
}

View File

@ -0,0 +1,71 @@
package sql
import (
"bytes"
"context"
"fmt"
log "github.com/sirupsen/logrus"
"image/png"
"time"
"github.com/pquerna/otp/totp"
"github.com/authorizerdev/authorizer/server/crypto"
)
func (p *provider) GenerateTotp(ctx context.Context, id string) (*string, error) {
var buf bytes.Buffer
//get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("error while getting user details")
}
// generate totp, TOTP hash is valid for 30 seconds
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "authorizer",
AccountName: user.Email,
})
if err != nil {
return nil, fmt.Errorf("error while genrating totp")
}
// get secret for user
secret := key.Secret()
//generating image for key and encoding to base64 for displaying in frontend
img, err := key.Image(200, 200)
if err != nil {
return nil, fmt.Errorf("error while creating qr image for totp")
}
png.Encode(&buf, img)
encodedText := crypto.EncryptB64(buf.String())
// update user totp secret in db
user.UpdatedAt = time.Now().Unix()
user.TotpSecret = &secret
_, err = p.UpdateUser(ctx, user)
if err != nil {
return nil, fmt.Errorf("error while updating user's totp secret")
}
log.Info("\n\n\n", &encodedText)
return &encodedText, nil
}
func (p *provider) ValidatePasscode(ctx context.Context, passcode string, id string) (bool, error) {
// get user details
user, err := p.GetUserByID(ctx, id)
if err != nil {
return false, fmt.Errorf("error while getting user details")
}
status := totp.Validate(passcode, *user.TotpSecret)
if !user.TotpVerified {
if status {
user.TotpVerified = true
p.UpdateUser(ctx, user)
return status, nil
}
return status, nil
}
return status, nil
}

37
server/env/env.go vendored
View File

@ -104,6 +104,8 @@ func InitAllEnv() error {
osDisableStrongPassword := os.Getenv(constants.EnvKeyDisableStrongPassword)
osEnforceMultiFactorAuthentication := os.Getenv(constants.EnvKeyEnforceMultiFactorAuthentication)
osDisableMultiFactorAuthentication := os.Getenv(constants.EnvKeyDisableMultiFactorAuthentication)
osDisableTOTPLogin := os.Getenv(constants.EnvKeyDisableTOTPLogin)
osDisableMailOTPLogin := os.Getenv(constants.EnvKeyDisableMailOTPLogin)
// phone verification var
osDisablePhoneVerification := os.Getenv(constants.EnvKeyDisablePhoneVerification)
osDisablePlayground := os.Getenv(constants.EnvKeyDisablePlayGround)
@ -689,20 +691,13 @@ func InitAllEnv() error {
envData[constants.EnvKeyDisableEmailVerification] = true
envData[constants.EnvKeyDisableMagicLinkLogin] = true
envData[constants.EnvKeyIsEmailServiceEnabled] = false
envData[constants.EnvKeyDisableMailOTPLogin] = true
}
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) && !envData[constants.EnvKeyIsSMSServiceEnabled].(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) {
envData[constants.EnvKeyDisableMagicLinkLogin] = true
}
@ -840,6 +835,32 @@ func InitAllEnv() error {
}
}
if _, ok := envData[constants.EnvKeyDisableTOTPLogin]; !ok {
envData[constants.EnvKeyDisableTOTPLogin] = osDisableTOTPLogin == "false"
}
if osDisableTOTPLogin != "" {
boolValue, err := strconv.ParseBool(osDisableTOTPLogin)
if err != nil {
return err
}
if boolValue != envData[constants.EnvKeyDisableTOTPLogin].(bool) {
envData[constants.EnvKeyDisableTOTPLogin] = boolValue
}
}
if _, ok := envData[constants.EnvKeyDisableMailOTPLogin]; !ok {
envData[constants.EnvKeyDisableMailOTPLogin] = osDisableMailOTPLogin == "true"
}
if osDisableMailOTPLogin != "" {
boolValue, err := strconv.ParseBool(osDisableMailOTPLogin)
if err != nil {
return err
}
if boolValue != envData[constants.EnvKeyDisableMailOTPLogin].(bool) {
envData[constants.EnvKeyDisableMailOTPLogin] = boolValue
}
}
err = memorystore.Provider.UpdateEnvStore(envData)
if err != nil {
log.Debug("Error while updating env store: ", err)

View File

@ -196,7 +196,7 @@ func PersistEnv() error {
envValue := strings.TrimSpace(os.Getenv(key))
if envValue != "" {
switch key {
case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableMobileBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyIsSMSServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication, constants.EnvKeyDisableMultiFactorAuthentication, constants.EnvKeyAdminCookieSecure, constants.EnvKeyAppCookieSecure, constants.EnvKeyDisablePhoneVerification, constants.EnvKeyDisablePlayGround:
case constants.EnvKeyIsProd, constants.EnvKeyDisableBasicAuthentication, constants.EnvKeyDisableMobileBasicAuthentication, constants.EnvKeyDisableEmailVerification, constants.EnvKeyDisableLoginPage, constants.EnvKeyDisableMagicLinkLogin, constants.EnvKeyDisableSignUp, constants.EnvKeyDisableRedisForEnv, constants.EnvKeyDisableStrongPassword, constants.EnvKeyIsEmailServiceEnabled, constants.EnvKeyIsSMSServiceEnabled, constants.EnvKeyEnforceMultiFactorAuthentication, constants.EnvKeyDisableMultiFactorAuthentication, constants.EnvKeyAdminCookieSecure, constants.EnvKeyAppCookieSecure, constants.EnvKeyDisablePhoneVerification, constants.EnvKeyDisablePlayGround, constants.EnvKeyDisableTOTPLogin, constants.EnvKeyDisableMailOTPLogin:
if envValueBool, err := strconv.ParseBool(envValue); err == nil {
if value.(bool) != envValueBool {
storeData[key] = envValueBool
@ -227,6 +227,11 @@ func PersistEnv() error {
storeData[constants.EnvKeyDisableMagicLinkLogin] = true
hasChanged = true
}
if !storeData[constants.EnvKeyDisableMailOTPLogin].(bool) {
storeData[constants.EnvKeyDisableMailOTPLogin] = true
hasChanged = true
}
}
err = memorystore.Provider.UpdateEnvStore(storeData)

View File

@ -13,6 +13,7 @@ require (
github.com/go-playground/validator/v10 v10.11.1 // indirect
github.com/goccy/go-json v0.9.11 // indirect
github.com/gocql/gocql v1.2.0
github.com/gokyle/twofactor v1.0.1
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
@ -21,10 +22,12 @@ require (
github.com/joho/godotenv v1.3.0
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/v9 v9.0.3
github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.8.0
github.com/tuotoo/qrcode v0.0.0-20220425170535-52ccc2bebf5d
github.com/twilio/twilio-go v1.7.2
github.com/vektah/gqlparser/v2 v2.5.1
go.mongodb.org/mongo-driver v1.8.1

View File

@ -56,8 +56,12 @@ github.com/aws/aws-sdk-go v1.44.298/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
github.com/bits-and-blooms/bitset v1.2.1 h1:M+/hrU9xlMp7t4TyTDQW97d3tRPVuKFC6zBEK16QnXY=
github.com/bits-and-blooms/bitset v1.2.1/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
@ -129,6 +133,8 @@ github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gocql/gocql v1.2.0 h1:TZhsCd7fRuye4VyHr3WCvWwIQaZUmjsqnSIXK9FcVCE=
github.com/gocql/gocql v1.2.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8=
github.com/gokyle/twofactor v1.0.1 h1:uRhvx0S4Hb82RPIDALnf7QxbmPL49LyyaCkJDpWx+Ek=
github.com/gokyle/twofactor v1.0.1/go.mod h1:4gxzH1eaE/F3Pct/sCDNOylP0ClofUO5j4XZN9tKtLE=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
@ -254,6 +260,8 @@ github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ic
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q=
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw=
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
github.com/maruel/rs v1.1.0 h1:dh4OceAF5yD06EASOrb+DS358LI4g0B90YApSdjCP6U=
github.com/maruel/rs v1.1.0/go.mod h1:vzwMjzSJJxLIXmU62qHj6O5QRn5kvCKxFrfaFCxBcUY=
github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
@ -285,6 +293,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k=
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
@ -319,6 +329,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tuotoo/qrcode v0.0.0-20220425170535-52ccc2bebf5d h1:4x1FeGJRB00cvxnKXnRJDT89fvG/Lzm2ecm0vlr/qDs=
github.com/tuotoo/qrcode v0.0.0-20220425170535-52ccc2bebf5d/go.mod h1:uSELzeIcTceNCgzbKdJuJa0ouCqqtkyzL+6bnA3rM+M=
github.com/twilio/twilio-go v1.7.2 h1:tX38DXbSuDWWIK+tKAE2AJSMR6d8i7lf9ksY8J29VLE=
github.com/twilio/twilio-go v1.7.2/go.mod h1:tdnfQ5TjbewoAu4lf9bMsGvfuJ/QU9gYuv9yx3TSIXU=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
@ -743,5 +755,7 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.6.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -52,6 +52,8 @@ type ComplexityRoot struct {
RefreshToken func(childComplexity int) int
ShouldShowEmailOtpScreen func(childComplexity int) int
ShouldShowMobileOtpScreen func(childComplexity int) int
TotpBase64URL func(childComplexity int) int
TotpToken func(childComplexity int) int
User func(childComplexity int) int
}
@ -96,11 +98,13 @@ type ComplexityRoot struct {
DisableEmailVerification func(childComplexity int) int
DisableLoginPage func(childComplexity int) int
DisableMagicLinkLogin func(childComplexity int) int
DisableMailOtpLogin func(childComplexity int) int
DisableMultiFactorAuthentication func(childComplexity int) int
DisablePlayground func(childComplexity int) int
DisableRedisForEnv func(childComplexity int) int
DisableSignUp func(childComplexity int) int
DisableStrongPassword func(childComplexity int) int
DisableTotpLogin func(childComplexity int) int
EnforceMultiFactorAuthentication func(childComplexity int) int
FacebookClientID func(childComplexity int) int
FacebookClientSecret func(childComplexity int) int
@ -201,6 +205,7 @@ type ComplexityRoot struct {
UpdateWebhook func(childComplexity int, params model.UpdateWebhookRequest) int
VerifyEmail func(childComplexity int, params model.VerifyEmailInput) int
VerifyOtp func(childComplexity int, params model.VerifyOTPRequest) int
VerifyTotp func(childComplexity int, params model.VerifyTOTPRequest) int
}
Pagination struct {
@ -265,6 +270,7 @@ type ComplexityRoot struct {
RevokedTimestamp func(childComplexity int) int
Roles func(childComplexity int) int
SignupMethods func(childComplexity int) int
TotpVerified func(childComplexity int) int
UpdatedAt func(childComplexity int) int
}
@ -347,6 +353,7 @@ type MutationResolver interface {
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)
VerifyTotp(ctx context.Context, params model.VerifyTOTPRequest) (*model.AuthResponse, error)
DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error)
UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error)
AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error)
@ -446,6 +453,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.AuthResponse.ShouldShowMobileOtpScreen(childComplexity), true
case "AuthResponse.totp_base64_url":
if e.complexity.AuthResponse.TotpBase64URL == nil {
break
}
return e.complexity.AuthResponse.TotpBase64URL(childComplexity), true
case "AuthResponse.totp_token":
if e.complexity.AuthResponse.TotpToken == nil {
break
}
return e.complexity.AuthResponse.TotpToken(childComplexity), true
case "AuthResponse.user":
if e.complexity.AuthResponse.User == nil {
break
@ -691,6 +712,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Env.DisableMagicLinkLogin(childComplexity), true
case "Env.DISABLE_MAIL_OTP_LOGIN":
if e.complexity.Env.DisableMailOtpLogin == nil {
break
}
return e.complexity.Env.DisableMailOtpLogin(childComplexity), true
case "Env.DISABLE_MULTI_FACTOR_AUTHENTICATION":
if e.complexity.Env.DisableMultiFactorAuthentication == nil {
break
@ -726,6 +754,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Env.DisableStrongPassword(childComplexity), true
case "Env.DISABLE_TOTP_LOGIN":
if e.complexity.Env.DisableTotpLogin == nil {
break
}
return e.complexity.Env.DisableTotpLogin(childComplexity), true
case "Env.ENFORCE_MULTI_FACTOR_AUTHENTICATION":
if e.complexity.Env.EnforceMultiFactorAuthentication == nil {
break
@ -1466,6 +1501,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.VerifyOtp(childComplexity, args["params"].(model.VerifyOTPRequest)), true
case "Mutation.verify_totp":
if e.complexity.Mutation.VerifyTotp == nil {
break
}
args, err := ec.field_Mutation_verify_totp_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.VerifyTotp(childComplexity, args["params"].(model.VerifyTOTPRequest)), true
case "Pagination.limit":
if e.complexity.Pagination.Limit == nil {
break
@ -1838,6 +1885,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.User.SignupMethods(childComplexity), true
case "User.totp_verified":
if e.complexity.User.TotpVerified == nil {
break
}
return e.complexity.User.TotpVerified(childComplexity), true
case "User.updated_at":
if e.complexity.User.UpdatedAt == nil {
break
@ -2139,6 +2193,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputValidateSessionInput,
ec.unmarshalInputVerifyEmailInput,
ec.unmarshalInputVerifyOTPRequest,
ec.unmarshalInputVerifyTOTPRequest,
ec.unmarshalInputWebhookRequest,
)
first := true
@ -2254,6 +2309,7 @@ type User {
revoked_timestamp: Int64
is_multi_factor_auth_enabled: Boolean
app_data: Map
totp_verified: Boolean
}
type Users {
@ -2301,6 +2357,8 @@ type AuthResponse {
refresh_token: String
expires_in: Int64
user: User
totp_base64_url: String
totp_token: String
}
type Response {
@ -2375,6 +2433,8 @@ type Env {
DEFAULT_AUTHORIZE_RESPONSE_TYPE: String
DEFAULT_AUTHORIZE_RESPONSE_MODE: String
DISABLE_PLAYGROUND: Boolean!
DISABLE_MAIL_OTP_LOGIN: Boolean!
DISABLE_TOTP_LOGIN: Boolean!
}
type ValidateJWTTokenResponse {
@ -2498,6 +2558,8 @@ input UpdateEnvInput {
DEFAULT_AUTHORIZE_RESPONSE_TYPE: String
DEFAULT_AUTHORIZE_RESPONSE_MODE: String
DISABLE_PLAYGROUND: Boolean
DISABLE_MAIL_OTP_LOGIN: Boolean
DISABLE_TOTP_LOGIN: Boolean
}
input AdminLoginInput {
@ -2760,6 +2822,12 @@ input VerifyOTPRequest {
state: String
}
input VerifyTOTPRequest {
otp: String!
token: String!
state: String
}
input ResendOTPRequest {
email: String
phone_number: String
@ -2789,6 +2857,7 @@ type Mutation {
revoke(params: OAuthRevokeInput!): Response!
verify_otp(params: VerifyOTPRequest!): AuthResponse!
resend_otp(params: ResendOTPRequest!): Response!
verify_totp(params: VerifyTOTPRequest!): AuthResponse!
# admin only apis
_delete_user(params: DeleteUserInput!): Response!
_update_user(params: UpdateUserInput!): User!
@ -3269,6 +3338,21 @@ func (ec *executionContext) field_Mutation_verify_otp_args(ctx context.Context,
return args, nil
}
func (ec *executionContext) field_Mutation_verify_totp_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 model.VerifyTOTPRequest
if tmp, ok := rawArgs["params"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params"))
arg0, err = ec.unmarshalNVerifyTOTPRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐVerifyTOTPRequest(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) {
var err error
args := map[string]interface{}{}
@ -3838,6 +3922,8 @@ func (ec *executionContext) fieldContext_AuthResponse_user(ctx context.Context,
return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field)
case "app_data":
return ec.fieldContext_User_app_data(ctx, field)
case "totp_verified":
return ec.fieldContext_User_totp_verified(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type User", field.Name)
},
@ -3845,6 +3931,88 @@ func (ec *executionContext) fieldContext_AuthResponse_user(ctx context.Context,
return fc, nil
}
func (ec *executionContext) _AuthResponse_totp_base64_url(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_AuthResponse_totp_base64_url(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.TotpBase64URL, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_AuthResponse_totp_base64_url(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "AuthResponse",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _AuthResponse_totp_token(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_AuthResponse_totp_token(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.TotpToken, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_AuthResponse_totp_token(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "AuthResponse",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _EmailTemplate_id(ctx context.Context, field graphql.CollectedField, obj *model.EmailTemplate) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_EmailTemplate_id(ctx, field)
if err != nil {
@ -6845,6 +7013,94 @@ func (ec *executionContext) fieldContext_Env_DISABLE_PLAYGROUND(ctx context.Cont
return fc, nil
}
func (ec *executionContext) _Env_DISABLE_MAIL_OTP_LOGIN(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Env_DISABLE_MAIL_OTP_LOGIN(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.DisableMailOtpLogin, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Env_DISABLE_MAIL_OTP_LOGIN(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Env",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Boolean does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Env_DISABLE_TOTP_LOGIN(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Env_DISABLE_TOTP_LOGIN(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.DisableTotpLogin, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Env_DISABLE_TOTP_LOGIN(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Env",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Boolean does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Error_message(ctx context.Context, field graphql.CollectedField, obj *model.Error) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Error_message(ctx, field)
if err != nil {
@ -7179,6 +7435,8 @@ func (ec *executionContext) fieldContext_InviteMembersResponse_Users(ctx context
return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field)
case "app_data":
return ec.fieldContext_User_app_data(ctx, field)
case "totp_verified":
return ec.fieldContext_User_totp_verified(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type User", field.Name)
},
@ -7901,6 +8159,10 @@ func (ec *executionContext) fieldContext_Mutation_signup(ctx context.Context, fi
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
case "user":
return ec.fieldContext_AuthResponse_user(ctx, field)
case "totp_base64_url":
return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field)
case "totp_token":
return ec.fieldContext_AuthResponse_totp_token(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
},
@ -7974,6 +8236,10 @@ func (ec *executionContext) fieldContext_Mutation_mobile_signup(ctx context.Cont
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
case "user":
return ec.fieldContext_AuthResponse_user(ctx, field)
case "totp_base64_url":
return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field)
case "totp_token":
return ec.fieldContext_AuthResponse_totp_token(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
},
@ -8047,6 +8313,10 @@ func (ec *executionContext) fieldContext_Mutation_login(ctx context.Context, fie
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
case "user":
return ec.fieldContext_AuthResponse_user(ctx, field)
case "totp_base64_url":
return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field)
case "totp_token":
return ec.fieldContext_AuthResponse_totp_token(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
},
@ -8120,6 +8390,10 @@ func (ec *executionContext) fieldContext_Mutation_mobile_login(ctx context.Conte
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
case "user":
return ec.fieldContext_AuthResponse_user(ctx, field)
case "totp_base64_url":
return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field)
case "totp_token":
return ec.fieldContext_AuthResponse_totp_token(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
},
@ -8359,6 +8633,10 @@ func (ec *executionContext) fieldContext_Mutation_verify_email(ctx context.Conte
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
case "user":
return ec.fieldContext_AuthResponse_user(ctx, field)
case "totp_base64_url":
return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field)
case "totp_token":
return ec.fieldContext_AuthResponse_totp_token(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
},
@ -8668,6 +8946,10 @@ func (ec *executionContext) fieldContext_Mutation_verify_otp(ctx context.Context
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
case "user":
return ec.fieldContext_AuthResponse_user(ctx, field)
case "totp_base64_url":
return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field)
case "totp_token":
return ec.fieldContext_AuthResponse_totp_token(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
},
@ -8745,6 +9027,83 @@ func (ec *executionContext) fieldContext_Mutation_resend_otp(ctx context.Context
return fc, nil
}
func (ec *executionContext) _Mutation_verify_totp(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_verify_totp(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().VerifyTotp(rctx, fc.Args["params"].(model.VerifyTOTPRequest))
})
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) fieldContext_Mutation_verify_totp(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "message":
return ec.fieldContext_AuthResponse_message(ctx, field)
case "should_show_email_otp_screen":
return ec.fieldContext_AuthResponse_should_show_email_otp_screen(ctx, field)
case "should_show_mobile_otp_screen":
return ec.fieldContext_AuthResponse_should_show_mobile_otp_screen(ctx, field)
case "access_token":
return ec.fieldContext_AuthResponse_access_token(ctx, field)
case "id_token":
return ec.fieldContext_AuthResponse_id_token(ctx, field)
case "refresh_token":
return ec.fieldContext_AuthResponse_refresh_token(ctx, field)
case "expires_in":
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
case "user":
return ec.fieldContext_AuthResponse_user(ctx, field)
case "totp_base64_url":
return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field)
case "totp_token":
return ec.fieldContext_AuthResponse_totp_token(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_verify_totp_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return
}
return fc, nil
}
func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation__delete_user(ctx, field)
if err != nil {
@ -8883,6 +9242,8 @@ func (ec *executionContext) fieldContext_Mutation__update_user(ctx context.Conte
return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field)
case "app_data":
return ec.fieldContext_User_app_data(ctx, field)
case "totp_verified":
return ec.fieldContext_User_totp_verified(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type User", field.Name)
},
@ -10090,6 +10451,10 @@ func (ec *executionContext) fieldContext_Query_session(ctx context.Context, fiel
return ec.fieldContext_AuthResponse_expires_in(ctx, field)
case "user":
return ec.fieldContext_AuthResponse_user(ctx, field)
case "totp_base64_url":
return ec.fieldContext_AuthResponse_totp_base64_url(ctx, field)
case "totp_token":
return ec.fieldContext_AuthResponse_totp_token(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type AuthResponse", field.Name)
},
@ -10187,6 +10552,8 @@ func (ec *executionContext) fieldContext_Query_profile(ctx context.Context, fiel
return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field)
case "app_data":
return ec.fieldContext_User_app_data(ctx, field)
case "totp_verified":
return ec.fieldContext_User_totp_verified(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type User", field.Name)
},
@ -10456,6 +10823,8 @@ func (ec *executionContext) fieldContext_Query__user(ctx context.Context, field
return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field)
case "app_data":
return ec.fieldContext_User_app_data(ctx, field)
case "totp_verified":
return ec.fieldContext_User_totp_verified(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type User", field.Name)
},
@ -10746,6 +11115,10 @@ func (ec *executionContext) fieldContext_Query__env(ctx context.Context, field g
return ec.fieldContext_Env_DEFAULT_AUTHORIZE_RESPONSE_MODE(ctx, field)
case "DISABLE_PLAYGROUND":
return ec.fieldContext_Env_DISABLE_PLAYGROUND(ctx, field)
case "DISABLE_MAIL_OTP_LOGIN":
return ec.fieldContext_Env_DISABLE_MAIL_OTP_LOGIN(ctx, field)
case "DISABLE_TOTP_LOGIN":
return ec.fieldContext_Env_DISABLE_TOTP_LOGIN(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Env", field.Name)
},
@ -12360,6 +12733,47 @@ func (ec *executionContext) fieldContext_User_app_data(ctx context.Context, fiel
return fc, nil
}
func (ec *executionContext) _User_totp_verified(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_totp_verified(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.TotpVerified, 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) fieldContext_User_totp_verified(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "User",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Boolean does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Users_pagination(ctx context.Context, field graphql.CollectedField, obj *model.Users) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Users_pagination(ctx, field)
if err != nil {
@ -12493,6 +12907,8 @@ func (ec *executionContext) fieldContext_Users_users(ctx context.Context, field
return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field)
case "app_data":
return ec.fieldContext_User_app_data(ctx, field)
case "totp_verified":
return ec.fieldContext_User_totp_verified(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type User", field.Name)
},
@ -12708,6 +13124,8 @@ func (ec *executionContext) fieldContext_ValidateSessionResponse_user(ctx contex
return ec.fieldContext_User_is_multi_factor_auth_enabled(ctx, field)
case "app_data":
return ec.fieldContext_User_app_data(ctx, field)
case "totp_verified":
return ec.fieldContext_User_totp_verified(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type User", field.Name)
},
@ -17132,7 +17550,7 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob
asMap[k] = v
}
fieldsInOrder := [...]string{"ACCESS_TOKEN_EXPIRY_TIME", "ADMIN_SECRET", "CUSTOM_ACCESS_TOKEN_SCRIPT", "OLD_ADMIN_SECRET", "SMTP_HOST", "SMTP_PORT", "SMTP_USERNAME", "SMTP_PASSWORD", "SMTP_LOCAL_NAME", "SENDER_EMAIL", "SENDER_NAME", "JWT_TYPE", "JWT_SECRET", "JWT_PRIVATE_KEY", "JWT_PUBLIC_KEY", "ALLOWED_ORIGINS", "APP_URL", "RESET_PASSWORD_URL", "APP_COOKIE_SECURE", "ADMIN_COOKIE_SECURE", "DISABLE_EMAIL_VERIFICATION", "DISABLE_BASIC_AUTHENTICATION", "DISABLE_MAGIC_LINK_LOGIN", "DISABLE_LOGIN_PAGE", "DISABLE_SIGN_UP", "DISABLE_REDIS_FOR_ENV", "DISABLE_STRONG_PASSWORD", "DISABLE_MULTI_FACTOR_AUTHENTICATION", "ENFORCE_MULTI_FACTOR_AUTHENTICATION", "ROLES", "PROTECTED_ROLES", "DEFAULT_ROLES", "JWT_ROLE_CLAIM", "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "FACEBOOK_CLIENT_ID", "FACEBOOK_CLIENT_SECRET", "LINKEDIN_CLIENT_ID", "LINKEDIN_CLIENT_SECRET", "APPLE_CLIENT_ID", "APPLE_CLIENT_SECRET", "TWITTER_CLIENT_ID", "TWITTER_CLIENT_SECRET", "MICROSOFT_CLIENT_ID", "MICROSOFT_CLIENT_SECRET", "MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID", "ORGANIZATION_NAME", "ORGANIZATION_LOGO", "DEFAULT_AUTHORIZE_RESPONSE_TYPE", "DEFAULT_AUTHORIZE_RESPONSE_MODE", "DISABLE_PLAYGROUND"}
fieldsInOrder := [...]string{"ACCESS_TOKEN_EXPIRY_TIME", "ADMIN_SECRET", "CUSTOM_ACCESS_TOKEN_SCRIPT", "OLD_ADMIN_SECRET", "SMTP_HOST", "SMTP_PORT", "SMTP_USERNAME", "SMTP_PASSWORD", "SMTP_LOCAL_NAME", "SENDER_EMAIL", "SENDER_NAME", "JWT_TYPE", "JWT_SECRET", "JWT_PRIVATE_KEY", "JWT_PUBLIC_KEY", "ALLOWED_ORIGINS", "APP_URL", "RESET_PASSWORD_URL", "APP_COOKIE_SECURE", "ADMIN_COOKIE_SECURE", "DISABLE_EMAIL_VERIFICATION", "DISABLE_BASIC_AUTHENTICATION", "DISABLE_MAGIC_LINK_LOGIN", "DISABLE_LOGIN_PAGE", "DISABLE_SIGN_UP", "DISABLE_REDIS_FOR_ENV", "DISABLE_STRONG_PASSWORD", "DISABLE_MULTI_FACTOR_AUTHENTICATION", "ENFORCE_MULTI_FACTOR_AUTHENTICATION", "ROLES", "PROTECTED_ROLES", "DEFAULT_ROLES", "JWT_ROLE_CLAIM", "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET", "FACEBOOK_CLIENT_ID", "FACEBOOK_CLIENT_SECRET", "LINKEDIN_CLIENT_ID", "LINKEDIN_CLIENT_SECRET", "APPLE_CLIENT_ID", "APPLE_CLIENT_SECRET", "TWITTER_CLIENT_ID", "TWITTER_CLIENT_SECRET", "MICROSOFT_CLIENT_ID", "MICROSOFT_CLIENT_SECRET", "MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID", "ORGANIZATION_NAME", "ORGANIZATION_LOGO", "DEFAULT_AUTHORIZE_RESPONSE_TYPE", "DEFAULT_AUTHORIZE_RESPONSE_MODE", "DISABLE_PLAYGROUND", "DISABLE_MAIL_OTP_LOGIN", "DISABLE_TOTP_LOGIN"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
@ -17563,6 +17981,22 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob
if err != nil {
return it, err
}
case "DISABLE_MAIL_OTP_LOGIN":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("DISABLE_MAIL_OTP_LOGIN"))
it.DisableMailOtpLogin, err = ec.unmarshalOBoolean2ᚖbool(ctx, v)
if err != nil {
return it, err
}
case "DISABLE_TOTP_LOGIN":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("DISABLE_TOTP_LOGIN"))
it.DisableTotpLogin, err = ec.unmarshalOBoolean2ᚖbool(ctx, v)
if err != nil {
return it, err
}
}
}
@ -18069,6 +18503,50 @@ func (ec *executionContext) unmarshalInputVerifyOTPRequest(ctx context.Context,
return it, nil
}
func (ec *executionContext) unmarshalInputVerifyTOTPRequest(ctx context.Context, obj interface{}) (model.VerifyTOTPRequest, error) {
var it model.VerifyTOTPRequest
asMap := map[string]interface{}{}
for k, v := range obj.(map[string]interface{}) {
asMap[k] = v
}
fieldsInOrder := [...]string{"otp", "token", "state"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
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
}
case "token":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("token"))
it.Token, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
case "state":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputWebhookRequest(ctx context.Context, obj interface{}) (model.WebhookRequest, error) {
var it model.WebhookRequest
asMap := map[string]interface{}{}
@ -18150,6 +18628,14 @@ func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.Selection
out.Values[i] = ec._AuthResponse_user(ctx, field, obj)
case "totp_base64_url":
out.Values[i] = ec._AuthResponse_totp_base64_url(ctx, field, obj)
case "totp_token":
out.Values[i] = ec._AuthResponse_totp_token(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@ -18557,6 +19043,20 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj
out.Values[i] = ec._Env_DISABLE_PLAYGROUND(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "DISABLE_MAIL_OTP_LOGIN":
out.Values[i] = ec._Env_DISABLE_MAIL_OTP_LOGIN(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "DISABLE_TOTP_LOGIN":
out.Values[i] = ec._Env_DISABLE_TOTP_LOGIN(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
@ -18942,6 +19442,15 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
return ec._Mutation_resend_otp(ctx, field)
})
if out.Values[i] == graphql.Null {
invalids++
}
case "verify_totp":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_verify_totp(ctx, field)
})
if out.Values[i] == graphql.Null {
invalids++
}
@ -19744,6 +20253,10 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
out.Values[i] = ec._User_app_data(ctx, field, obj)
case "totp_verified":
out.Values[i] = ec._User_totp_verified(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@ -21024,6 +21537,11 @@ func (ec *executionContext) unmarshalNVerifyOTPRequest2githubᚗcomᚋauthorizer
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) unmarshalNVerifyTOTPRequest2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐVerifyTOTPRequest(ctx context.Context, v interface{}) (model.VerifyTOTPRequest, error) {
res, err := ec.unmarshalInputVerifyTOTPRequest(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 {
return ec._Webhook(ctx, sel, &v)
}

View File

@ -34,6 +34,8 @@ type AuthResponse struct {
RefreshToken *string `json:"refresh_token"`
ExpiresIn *int64 `json:"expires_in"`
User *User `json:"user"`
TotpBase64URL *string `json:"totp_base64_url"`
TotpToken *string `json:"totp_token"`
}
type DeleteEmailTemplateRequest struct {
@ -122,6 +124,8 @@ type Env struct {
DefaultAuthorizeResponseType *string `json:"DEFAULT_AUTHORIZE_RESPONSE_TYPE"`
DefaultAuthorizeResponseMode *string `json:"DEFAULT_AUTHORIZE_RESPONSE_MODE"`
DisablePlayground bool `json:"DISABLE_PLAYGROUND"`
DisableMailOtpLogin bool `json:"DISABLE_MAIL_OTP_LOGIN"`
DisableTotpLogin bool `json:"DISABLE_TOTP_LOGIN"`
}
type Error struct {
@ -381,6 +385,8 @@ type UpdateEnvInput struct {
DefaultAuthorizeResponseType *string `json:"DEFAULT_AUTHORIZE_RESPONSE_TYPE"`
DefaultAuthorizeResponseMode *string `json:"DEFAULT_AUTHORIZE_RESPONSE_MODE"`
DisablePlayground *bool `json:"DISABLE_PLAYGROUND"`
DisableMailOtpLogin *bool `json:"DISABLE_MAIL_OTP_LOGIN"`
DisableTotpLogin *bool `json:"DISABLE_TOTP_LOGIN"`
}
type UpdateProfileInput struct {
@ -447,6 +453,7 @@ type User struct {
RevokedTimestamp *int64 `json:"revoked_timestamp"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled"`
AppData map[string]interface{} `json:"app_data"`
TotpVerified *bool `json:"totp_verified"`
}
type Users struct {
@ -504,6 +511,12 @@ type VerifyOTPRequest struct {
State *string `json:"state"`
}
type VerifyTOTPRequest struct {
Otp string `json:"otp"`
Token string `json:"token"`
State *string `json:"state"`
}
type Webhook struct {
ID string `json:"id"`
EventName *string `json:"event_name"`

View File

@ -52,6 +52,7 @@ type User {
revoked_timestamp: Int64
is_multi_factor_auth_enabled: Boolean
app_data: Map
totp_verified: Boolean
}
type Users {
@ -99,6 +100,8 @@ type AuthResponse {
refresh_token: String
expires_in: Int64
user: User
totp_base64_url: String
totp_token: String
}
type Response {
@ -173,6 +176,8 @@ type Env {
DEFAULT_AUTHORIZE_RESPONSE_TYPE: String
DEFAULT_AUTHORIZE_RESPONSE_MODE: String
DISABLE_PLAYGROUND: Boolean!
DISABLE_MAIL_OTP_LOGIN: Boolean!
DISABLE_TOTP_LOGIN: Boolean!
}
type ValidateJWTTokenResponse {
@ -296,6 +301,8 @@ input UpdateEnvInput {
DEFAULT_AUTHORIZE_RESPONSE_TYPE: String
DEFAULT_AUTHORIZE_RESPONSE_MODE: String
DISABLE_PLAYGROUND: Boolean
DISABLE_MAIL_OTP_LOGIN: Boolean
DISABLE_TOTP_LOGIN: Boolean
}
input AdminLoginInput {
@ -558,6 +565,12 @@ input VerifyOTPRequest {
state: String
}
input VerifyTOTPRequest {
otp: String!
token: String!
state: String
}
input ResendOTPRequest {
email: String
phone_number: String
@ -587,6 +600,7 @@ type Mutation {
revoke(params: OAuthRevokeInput!): Response!
verify_otp(params: VerifyOTPRequest!): AuthResponse!
resend_otp(params: ResendOTPRequest!): Response!
verify_totp(params: VerifyTOTPRequest!): AuthResponse!
# admin only apis
_delete_user(params: DeleteUserInput!): Response!
_update_user(params: UpdateUserInput!): User!

View File

@ -81,6 +81,11 @@ func (r *mutationResolver) ResendOtp(ctx context.Context, params model.ResendOTP
return resolvers.ResendOTPResolver(ctx, params)
}
// VerifyTotp is the resolver for the verify_totp field.
func (r *mutationResolver) VerifyTotp(ctx context.Context, params model.VerifyTOTPRequest) (*model.AuthResponse, error) {
return resolvers.VerifyTotpResolver(ctx, params)
}
// DeleteUser is the resolver for the _delete_user field.
func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) {
return resolvers.DeleteUserResolver(ctx, params)

View File

@ -36,9 +36,11 @@ func InitMemStore() error {
constants.EnvKeyIsSMSServiceEnabled: false,
constants.EnvKeyEnforceMultiFactorAuthentication: false,
constants.EnvKeyDisableMultiFactorAuthentication: false,
constants.EnvKeyDisableTOTPLogin: false,
constants.EnvKeyAppCookieSecure: true,
constants.EnvKeyAdminCookieSecure: true,
constants.EnvKeyDisablePlayGround: true,
constants.EnvKeyDisableMailOTPLogin: true,
}
requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv()

View File

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

View File

@ -203,6 +203,8 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
res.AdminCookieSecure = store[constants.EnvKeyAdminCookieSecure].(bool)
res.AppCookieSecure = store[constants.EnvKeyAppCookieSecure].(bool)
res.DisablePlayground = store[constants.EnvKeyDisablePlayGround].(bool)
res.DisableMailOtpLogin = store[constants.EnvKeyDisableMailOTPLogin].(bool)
res.DisableTotpLogin = store[constants.EnvKeyDisableTOTPLogin].(bool)
return res, nil
}

View File

@ -7,11 +7,13 @@ import (
"time"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt"
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/crypto"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/email"
@ -110,8 +112,18 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
log.Debug("MFA service not enabled: ", err)
}
isTOTPLoginDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableTOTPLogin)
if err != nil || !isTOTPLoginDisabled {
log.Debug("totp service not enabled: ", err)
}
isMailOTPDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableMailOTPLogin)
if err != nil || !isMailOTPDisabled {
log.Debug("mail OTP service not enabled: ", err)
}
// If email service is not enabled continue the process in any way
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && isEmailServiceEnabled && !isMFADisabled {
if refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isMailOTPDisabled && !isMFADisabled {
otp := utils.GenerateOTP()
expires := time.Now().Add(1 * time.Minute).Unix()
otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{
@ -150,6 +162,47 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
}, nil
}
// if mfa enabled and also totp enabled
if !isMFADisabled && refs.BoolValue(user.IsMultiFactorAuthEnabled) && !isTOTPLoginDisabled {
pubKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey)
if err != nil {
log.Debug("error while getting public key")
}
publicKey, err := crypto.ParseRsaPublicKeyFromPemStr(pubKey)
if err != nil {
log.Debug("error while parsing public key")
}
//encrypting user id, so it can be used as token for verifying
encryptedUserId, err := crypto.EncryptRSA(user.ID, *publicKey)
if err != nil {
log.Debug("error while encrypting user id")
}
// for first time user or whose totp is not verified
if !user.TotpVerified {
base64URL, err := db.Provider.GenerateTotp(ctx, user.ID)
if err != nil {
log.Debug("error while generating base64 url: ", err)
}
// when user is first time registering for totp
res = &model.AuthResponse{
Message: `Proceed to totp screen`,
TotpBase64URL: base64URL,
TotpToken: &encryptedUserId,
}
return res, nil
} else {
//when user is already register for totp
res = &model.AuthResponse{
Message: `Proceed to totp screen`,
TotpToken: &encryptedUserId,
}
return res, nil
}
}
code := ""
codeChallenge := ""
nonce := ""

View File

@ -253,13 +253,15 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
// 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] == "" {
updatedData[constants.EnvKeyIsEmailServiceEnabled] = false
updatedData[constants.EnvKeyDisableMultiFactorAuthentication] = true
if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) {
updatedData[constants.EnvKeyDisableEmailVerification] = true
}
if !updatedData[constants.EnvKeyDisableMailOTPLogin].(bool) {
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
}
if !updatedData[constants.EnvKeyDisableMagicLinkLogin].(bool) {
updatedData[constants.EnvKeyDisableMagicLinkLogin] = true
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
updatedData[constants.EnvKeyDisableTOTPLogin] = false
}
}
@ -274,6 +276,21 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
}
}
if updatedData[constants.EnvKeyDisableMultiFactorAuthentication].(bool) {
updatedData[constants.EnvKeyDisableTOTPLogin] = true
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
} else {
if !updatedData[constants.EnvKeyDisableMailOTPLogin].(bool) && !updatedData[constants.EnvKeyDisableTOTPLogin].(bool) {
errors.New("can't enable both mfa methods at same time")
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
updatedData[constants.EnvKeyDisableTOTPLogin] = false
} else if updatedData[constants.EnvKeyDisableMailOTPLogin].(bool) && updatedData[constants.EnvKeyDisableTOTPLogin].(bool) {
errors.New("can't disable both mfa methods at same time")
updatedData[constants.EnvKeyDisableMailOTPLogin] = true
updatedData[constants.EnvKeyDisableTOTPLogin] = false
}
}
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,

View File

@ -0,0 +1,136 @@
package resolvers
import (
"context"
"fmt"
"strings"
"time"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/crypto"
"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/refs"
"github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils"
)
// VerifyTotpResolver resolver for verify totp mutation
func VerifyTotpResolver(ctx context.Context, params model.VerifyTOTPRequest) (*model.AuthResponse, error) {
var res *model.AuthResponse
encryptedkey := params.Token
pvtKey, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyJwtPrivateKey)
if err != nil {
log.Debug("error while getting private key")
}
privateKey, err := crypto.ParseRsaPrivateKeyFromPemStr(pvtKey)
if err != nil {
log.Debug("error while parsing private key")
}
userID, err := crypto.DecryptRSA(encryptedkey, *privateKey)
if err != nil {
log.Debug("error while decrypting userId")
}
gc, err := utils.GinContextFromContext(ctx)
if err != nil {
log.Debug("Failed to get GinContext: ", err)
return res, err
}
user, err := db.Provider.GetUserByID(ctx, userID)
if err != nil {
return nil, err
}
status, err := db.Provider.ValidatePasscode(ctx, params.Otp, userID)
if err != nil || !status {
return nil, fmt.Errorf("error while validating passcode")
}
code := ""
codeChallenge := ""
nonce := ""
roles := strings.Split(user.Roles, ",")
scope := []string{"openid", "email", "profile"}
// Get state from store
if params.State != nil {
authorizeState, _ := memorystore.Provider.GetState(refs.StringValue(params.State))
if authorizeState != "" {
authorizeStateSplit := strings.Split(authorizeState, "@@")
if len(authorizeStateSplit) > 1 {
code = authorizeStateSplit[0]
codeChallenge = authorizeStateSplit[1]
} else {
nonce = authorizeState
}
go memorystore.Provider.RemoveState(refs.StringValue(params.State))
}
}
if nonce == "" {
nonce = uuid.New().String()
}
authToken, err := token.CreateAuthToken(gc, user, roles, scope, constants.AuthRecipeMethodBasicAuth, nonce, code)
if err != nil {
log.Debug("Failed to create auth token", err)
return res, err
}
// TODO add to other login options as well
// Code challenge could be optional if PKCE flow is not used
if code != "" {
if err := memorystore.Provider.SetState(code, codeChallenge+"@@"+authToken.FingerPrintHash); err != nil {
log.Debug("SetState failed: ", err)
return res, err
}
}
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
if expiresIn <= 0 {
expiresIn = 1
}
res = &model.AuthResponse{
Message: `Logged in successfully`,
AccessToken: &authToken.AccessToken.Token,
IDToken: &authToken.IDToken.Token,
ExpiresIn: &expiresIn,
User: user.AsAPIUser(),
}
cookie.SetSession(gc, authToken.FingerPrintHash)
sessionStoreKey := constants.AuthRecipeMethodBasicAuth + ":" + user.ID
memorystore.Provider.SetUserSession(sessionStoreKey, constants.TokenTypeSessionToken+"_"+authToken.FingerPrint, authToken.FingerPrintHash, authToken.SessionTokenExpiresAt)
memorystore.Provider.SetUserSession(sessionStoreKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token, authToken.AccessToken.ExpiresAt)
if authToken.RefreshToken != nil {
res.RefreshToken = &authToken.RefreshToken.Token
memorystore.Provider.SetUserSession(sessionStoreKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token, authToken.RefreshToken.ExpiresAt)
}
go func() {
utils.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodBasicAuth, user)
db.Provider.AddSession(ctx, &models.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(gc.Request),
IP: utils.GetIP(gc.Request),
})
}()
return res, nil
}

View File

@ -141,6 +141,7 @@ func TestResolvers(t *testing.T) {
inviteUserTest(t, s)
validateJwtTokenTest(t, s)
verifyOTPTest(t, s)
verifyTOTPTest(t, s)
resendOTPTest(t, s)
validateSessionTests(t, s)

View File

@ -54,6 +54,9 @@ func resendOTPTest(t *testing.T, s TestSetup) {
})
assert.NoError(t, err)
assert.NotNil(t, updateRes)
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableMailOTPLogin, false)
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableTOTPLogin, true)
// Resend otp should return error as no initial opt is being sent
resendOtpRes, err := resolvers.ResendOTPResolver(ctx, model.ResendOTPRequest{
Email: refs.NewStringRef(email),

View File

@ -54,6 +54,8 @@ func verifyOTPTest(t *testing.T, s TestSetup) {
})
assert.NoError(t, err)
assert.NotEmpty(t, updateProfileRes.Message)
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableMailOTPLogin, false)
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableTOTPLogin, true)
// Login should not return error but access token should be empty as otp should have been sent
loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{

View File

@ -0,0 +1,149 @@
package test
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"strings"
"testing"
"github.com/gokyle/twofactor"
"github.com/stretchr/testify/assert"
"github.com/tuotoo/qrcode"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/resolvers"
"github.com/authorizerdev/authorizer/server/token"
)
func verifyTOTPTest(t *testing.T, s TestSetup) {
t.Helper()
t.Run(`should verify totp`, func(t *testing.T) {
req, ctx := createContext(s)
email := "verify_totp." + s.TestInfo.Email
cleanData(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)
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableTOTPLogin, false)
memorystore.Provider.UpdateEnvVariable(constants.EnvKeyDisableMailOTPLogin, true)
// Login should not return error but access token should be empty
loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
})
assert.NoError(t, err)
assert.NotNil(t, loginRes)
assert.NotNil(t, loginRes.TotpBase64URL)
assert.NotNil(t, loginRes.TotpToken)
assert.Nil(t, loginRes.AccessToken)
assert.Equal(t, loginRes.Message, `Proceed to totp screen`)
// get totp url for validation
pngBytes, err := base64.StdEncoding.DecodeString(*loginRes.TotpBase64URL)
assert.NoError(t, err)
qrmatrix, err := qrcode.Decode(bytes.NewReader(pngBytes))
assert.NoError(t, err)
tf, label, err := twofactor.FromURL(qrmatrix.Content)
data := strings.Split(label, ":")
assert.NoError(t, err)
assert.Equal(t, email, data[1])
assert.NotNil(t, tf)
code := tf.OTP()
assert.NotEmpty(t, code)
valid, err := resolvers.VerifyTotpResolver(ctx, model.VerifyTOTPRequest{
Otp: code,
Token: *loginRes.TotpToken,
})
accessToken := *valid.AccessToken
assert.NoError(t, err)
assert.NotNil(t, accessToken)
assert.Equal(t, `Logged in successfully`, valid.Message)
assert.NotEmpty(t, accessToken)
claims, err := token.ParseJWTToken(accessToken)
assert.NoError(t, err)
assert.NotEmpty(t, claims)
loginMethod := claims["login_method"]
sessionKey := verifyRes.User.ID
if loginMethod != nil && loginMethod != "" {
sessionKey = loginMethod.(string) + ":" + verifyRes.User.ID
}
sessionToken, err := memorystore.Provider.GetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+claims["nonce"].(string))
assert.NoError(t, err)
assert.NotEmpty(t, sessionToken)
cookie := fmt.Sprintf("%s=%s;", constants.AppCookieName+"_session", sessionToken)
cookie = strings.TrimSuffix(cookie, ";")
req.Header.Set("Cookie", cookie)
//logged out
logout, err := resolvers.LogoutResolver(ctx)
assert.NoError(t, err)
assert.Equal(t, logout.Message, `Logged out successfully`)
loginRes, err = resolvers.LoginResolver(ctx, model.LoginInput{
Email: email,
Password: s.TestInfo.Password,
})
assert.NoError(t, err)
assert.NotNil(t, loginRes)
assert.NotNil(t, loginRes.TotpToken)
assert.Nil(t, loginRes.TotpBase64URL)
assert.Nil(t, loginRes.AccessToken)
assert.Equal(t, loginRes.Message, `Proceed to totp screen`)
code = tf.OTP()
assert.NotEmpty(t, code)
valid, err = resolvers.VerifyTotpResolver(ctx, model.VerifyTOTPRequest{
Otp: code,
Token: *loginRes.TotpToken,
})
assert.NoError(t, err)
assert.NotNil(t, *valid.AccessToken)
assert.Equal(t, `Logged in successfully`, valid.Message)
cleanData(email)
})
}