Merge branch 'authorizerdev/authorizer:main' into main
This commit is contained in:
@@ -69,6 +69,8 @@ const (
|
||||
EnvKeyDisableMagicLinkLogin = "DISABLE_MAGIC_LINK_LOGIN"
|
||||
// EnvKeyDisableLoginPage key for env variable DISABLE_LOGIN_PAGE
|
||||
EnvKeyDisableLoginPage = "DISABLE_LOGIN_PAGE"
|
||||
// EnvKeyDisableSignUp key for env variable DISABLE_SIGN_UP
|
||||
EnvKeyDisableSignUp = "DISABLE_SIGN_UP"
|
||||
// EnvKeyRoles key for env variable ROLES
|
||||
EnvKeyRoles = "ROLES"
|
||||
// EnvKeyProtectedRoles key for env variable PROTECTED_ROLES
|
||||
|
@@ -27,11 +27,16 @@ type User struct {
|
||||
Roles string `json:"roles" bson:"roles"`
|
||||
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||
RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp"`
|
||||
}
|
||||
|
||||
func (user *User) AsAPIUser() *model.User {
|
||||
isEmailVerified := user.EmailVerifiedAt != nil
|
||||
isPhoneVerified := user.PhoneNumberVerifiedAt != nil
|
||||
email := user.Email
|
||||
createdAt := user.CreatedAt
|
||||
updatedAt := user.UpdatedAt
|
||||
revokedTimestamp := user.RevokedTimestamp
|
||||
return &model.User{
|
||||
ID: user.ID,
|
||||
Email: user.Email,
|
||||
@@ -41,14 +46,15 @@ func (user *User) AsAPIUser() *model.User {
|
||||
FamilyName: user.FamilyName,
|
||||
MiddleName: user.MiddleName,
|
||||
Nickname: user.Nickname,
|
||||
PreferredUsername: &user.Email,
|
||||
PreferredUsername: &email,
|
||||
Gender: user.Gender,
|
||||
Birthdate: user.Birthdate,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
PhoneNumberVerified: &isPhoneVerified,
|
||||
Picture: user.Picture,
|
||||
Roles: strings.Split(user.Roles, ","),
|
||||
CreatedAt: &user.CreatedAt,
|
||||
UpdatedAt: &user.UpdatedAt,
|
||||
CreatedAt: &createdAt,
|
||||
UpdatedAt: &updatedAt,
|
||||
RevokedTimestamp: revokedTimestamp,
|
||||
}
|
||||
}
|
||||
|
@@ -17,15 +17,23 @@ type VerificationRequest struct {
|
||||
}
|
||||
|
||||
func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest {
|
||||
token := v.Token
|
||||
createdAt := v.CreatedAt
|
||||
updatedAt := v.UpdatedAt
|
||||
email := v.Email
|
||||
nonce := v.Nonce
|
||||
redirectURI := v.RedirectURI
|
||||
expires := v.ExpiresAt
|
||||
identifier := v.Identifier
|
||||
return &model.VerificationRequest{
|
||||
ID: v.ID,
|
||||
Token: &v.Token,
|
||||
Identifier: &v.Identifier,
|
||||
Expires: &v.ExpiresAt,
|
||||
CreatedAt: &v.CreatedAt,
|
||||
UpdatedAt: &v.UpdatedAt,
|
||||
Email: &v.Email,
|
||||
Nonce: &v.Nonce,
|
||||
RedirectURI: &v.RedirectURI,
|
||||
Token: &token,
|
||||
Identifier: &identifier,
|
||||
Expires: &expires,
|
||||
CreatedAt: &createdAt,
|
||||
UpdatedAt: &updatedAt,
|
||||
Email: &email,
|
||||
Nonce: &nonce,
|
||||
RedirectURI: &redirectURI,
|
||||
}
|
||||
}
|
||||
|
113
server/email/invite_email.go
Normal file
113
server/email/invite_email.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
)
|
||||
|
||||
// InviteEmail to send invite email
|
||||
func InviteEmail(toEmail, token, verificationURL, redirectURI string) error {
|
||||
// The receiver needs to be in slice as the receive supports multiple receiver
|
||||
Receiver := []string{toEmail}
|
||||
|
||||
Subject := "Please accept the invitation"
|
||||
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>Hi there 👋</p>
|
||||
<p>Join us! You are invited to sign-up for <b>{{.org_name}}</b>. Please accept the invitation by clicking the clicking the button below.</p> <br/>
|
||||
<a
|
||||
clicktracking="off" href="{{.verification_url}}" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Get Started</a>
|
||||
</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)
|
||||
data["org_logo"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
|
||||
data["org_name"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
|
||||
data["verification_url"] = verificationURL + "?token=" + token + "&redirect_uri=" + redirectURI
|
||||
message = addEmailTemplate(message, data, "invite_email.tmpl")
|
||||
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
|
||||
|
||||
err := SendMail(Receiver, Subject, message)
|
||||
if err != nil {
|
||||
log.Println("=> error sending email:", err)
|
||||
}
|
||||
return err
|
||||
}
|
1
server/env/env.go
vendored
1
server/env/env.go
vendored
@@ -285,6 +285,7 @@ func InitAllEnv() error {
|
||||
envData.BoolEnv[constants.EnvKeyDisableEmailVerification] = os.Getenv(constants.EnvKeyDisableEmailVerification) == "true"
|
||||
envData.BoolEnv[constants.EnvKeyDisableMagicLinkLogin] = os.Getenv(constants.EnvKeyDisableMagicLinkLogin) == "true"
|
||||
envData.BoolEnv[constants.EnvKeyDisableLoginPage] = os.Getenv(constants.EnvKeyDisableLoginPage) == "true"
|
||||
envData.BoolEnv[constants.EnvKeyDisableSignUp] = os.Getenv(constants.EnvKeyDisableSignUp) == "true"
|
||||
|
||||
// no need to add nil check as its already done above
|
||||
if envData.StringEnv[constants.EnvKeySmtpHost] == "" || envData.StringEnv[constants.EnvKeySmtpUsername] == "" || envData.StringEnv[constants.EnvKeySmtpPassword] == "" || envData.StringEnv[constants.EnvKeySenderEmail] == "" && envData.StringEnv[constants.EnvKeySmtpPort] == "" {
|
||||
|
@@ -41,6 +41,7 @@ var defaultStore = &EnvStore{
|
||||
constants.EnvKeyDisableMagicLinkLogin: false,
|
||||
constants.EnvKeyDisableEmailVerification: false,
|
||||
constants.EnvKeyDisableLoginPage: false,
|
||||
constants.EnvKeyDisableSignUp: false,
|
||||
},
|
||||
SliceEnv: map[string][]string{},
|
||||
},
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,7 @@ type Env struct {
|
||||
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
|
||||
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
|
||||
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
|
||||
DisableSignUp *bool `json:"DISABLE_SIGN_UP"`
|
||||
Roles []string `json:"ROLES"`
|
||||
ProtectedRoles []string `json:"PROTECTED_ROLES"`
|
||||
DefaultRoles []string `json:"DEFAULT_ROLES"`
|
||||
@@ -75,6 +76,21 @@ type ForgotPasswordInput struct {
|
||||
RedirectURI *string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type GenerateJWTKeysInput struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type GenerateJWTKeysResponse struct {
|
||||
Secret *string `json:"secret"`
|
||||
PublicKey *string `json:"public_key"`
|
||||
PrivateKey *string `json:"private_key"`
|
||||
}
|
||||
|
||||
type InviteMemberInput struct {
|
||||
Emails []string `json:"emails"`
|
||||
RedirectURI *string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type LoginInput struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
@@ -99,6 +115,7 @@ type Meta struct {
|
||||
IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"`
|
||||
IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"`
|
||||
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
|
||||
IsSignUpEnabled bool `json:"is_sign_up_enabled"`
|
||||
}
|
||||
|
||||
type OAuthRevokeInput struct {
|
||||
@@ -155,6 +172,11 @@ type SignUpInput struct {
|
||||
ConfirmPassword string `json:"confirm_password"`
|
||||
Roles []string `json:"roles"`
|
||||
Scope []string `json:"scope"`
|
||||
RedirectURI *string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type UpdateAccessInput struct {
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type UpdateEnvInput struct {
|
||||
@@ -180,6 +202,7 @@ type UpdateEnvInput struct {
|
||||
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
|
||||
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
|
||||
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
|
||||
DisableSignUp *bool `json:"DISABLE_SIGN_UP"`
|
||||
Roles []string `json:"ROLES"`
|
||||
ProtectedRoles []string `json:"PROTECTED_ROLES"`
|
||||
DefaultRoles []string `json:"DEFAULT_ROLES"`
|
||||
@@ -242,6 +265,7 @@ type User struct {
|
||||
Roles []string `json:"roles"`
|
||||
CreatedAt *int64 `json:"created_at"`
|
||||
UpdatedAt *int64 `json:"updated_at"`
|
||||
RevokedTimestamp *int64 `json:"revoked_timestamp"`
|
||||
}
|
||||
|
||||
type Users struct {
|
||||
@@ -249,6 +273,16 @@ type Users struct {
|
||||
Users []*User `json:"users"`
|
||||
}
|
||||
|
||||
type ValidateJWTTokenInput struct {
|
||||
TokenType string `json:"token_type"`
|
||||
Token string `json:"token"`
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
|
||||
type ValidateJWTTokenResponse struct {
|
||||
IsValid bool `json:"is_valid"`
|
||||
}
|
||||
|
||||
type VerificationRequest struct {
|
||||
ID string `json:"id"`
|
||||
Identifier *string `json:"identifier"`
|
||||
|
@@ -21,6 +21,7 @@ type Meta {
|
||||
is_email_verification_enabled: Boolean!
|
||||
is_basic_authentication_enabled: Boolean!
|
||||
is_magic_link_login_enabled: Boolean!
|
||||
is_sign_up_enabled: Boolean!
|
||||
}
|
||||
|
||||
type User {
|
||||
@@ -42,6 +43,7 @@ type User {
|
||||
roles: [String!]!
|
||||
created_at: Int64
|
||||
updated_at: Int64
|
||||
revoked_timestamp: Int64
|
||||
}
|
||||
|
||||
type Users {
|
||||
@@ -111,6 +113,7 @@ type Env {
|
||||
DISABLE_BASIC_AUTHENTICATION: Boolean
|
||||
DISABLE_MAGIC_LINK_LOGIN: Boolean
|
||||
DISABLE_LOGIN_PAGE: Boolean
|
||||
DISABLE_SIGN_UP: Boolean
|
||||
ROLES: [String!]
|
||||
PROTECTED_ROLES: [String!]
|
||||
DEFAULT_ROLES: [String!]
|
||||
@@ -125,6 +128,16 @@ type Env {
|
||||
ORGANIZATION_LOGO: String
|
||||
}
|
||||
|
||||
type ValidateJWTTokenResponse {
|
||||
is_valid: Boolean!
|
||||
}
|
||||
|
||||
type GenerateJWTKeysResponse {
|
||||
secret: String
|
||||
public_key: String
|
||||
private_key: String
|
||||
}
|
||||
|
||||
input UpdateEnvInput {
|
||||
ACCESS_TOKEN_EXPIRY_TIME: String
|
||||
ADMIN_SECRET: String
|
||||
@@ -148,6 +161,7 @@ input UpdateEnvInput {
|
||||
DISABLE_BASIC_AUTHENTICATION: Boolean
|
||||
DISABLE_MAGIC_LINK_LOGIN: Boolean
|
||||
DISABLE_LOGIN_PAGE: Boolean
|
||||
DISABLE_SIGN_UP: Boolean
|
||||
ROLES: [String!]
|
||||
PROTECTED_ROLES: [String!]
|
||||
DEFAULT_ROLES: [String!]
|
||||
@@ -184,6 +198,7 @@ input SignUpInput {
|
||||
confirm_password: String!
|
||||
roles: [String!]
|
||||
scope: [String!]
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
input LoginInput {
|
||||
@@ -274,6 +289,25 @@ input OAuthRevokeInput {
|
||||
refresh_token: String!
|
||||
}
|
||||
|
||||
input InviteMemberInput {
|
||||
emails: [String!]!
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
input UpdateAccessInput {
|
||||
user_id: String!
|
||||
}
|
||||
|
||||
input ValidateJWTTokenInput {
|
||||
token_type: String!
|
||||
token: String!
|
||||
roles: [String!]
|
||||
}
|
||||
|
||||
input GenerateJWTKeysInput {
|
||||
type: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
signup(params: SignUpInput!): AuthResponse!
|
||||
login(params: LoginInput!): AuthResponse!
|
||||
@@ -292,12 +326,17 @@ type Mutation {
|
||||
_admin_login(params: AdminLoginInput!): Response!
|
||||
_admin_logout: Response!
|
||||
_update_env(params: UpdateEnvInput!): Response!
|
||||
_invite_members(params: InviteMemberInput!): Response!
|
||||
_revoke_access(param: UpdateAccessInput!): Response!
|
||||
_enable_access(param: UpdateAccessInput!): Response!
|
||||
_generate_jwt_keys(params: GenerateJWTKeysInput!): GenerateJWTKeysResponse!
|
||||
}
|
||||
|
||||
type Query {
|
||||
meta: Meta!
|
||||
session(params: SessionQueryInput): AuthResponse!
|
||||
profile: User!
|
||||
validate_jwt_token(params: ValidateJWTTokenInput!): ValidateJWTTokenResponse!
|
||||
# admin only apis
|
||||
_users(params: PaginatedInput): Users!
|
||||
_verification_requests(params: PaginatedInput): VerificationRequests!
|
||||
|
@@ -75,6 +75,22 @@ func (r *mutationResolver) UpdateEnv(ctx context.Context, params model.UpdateEnv
|
||||
return resolvers.UpdateEnvResolver(ctx, params)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) {
|
||||
return resolvers.InviteMembersResolver(ctx, params)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RevokeAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) {
|
||||
return resolvers.RevokeAccessResolver(ctx, param)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EnableAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) {
|
||||
return resolvers.EnableAccessResolver(ctx, param)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GenerateJwtKeys(ctx context.Context, params model.GenerateJWTKeysInput) (*model.GenerateJWTKeysResponse, error) {
|
||||
return resolvers.GenerateJWTKeysResolver(ctx, params)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) {
|
||||
return resolvers.MetaResolver(ctx)
|
||||
}
|
||||
@@ -87,6 +103,10 @@ func (r *queryResolver) Profile(ctx context.Context) (*model.User, error) {
|
||||
return resolvers.ProfileResolver(ctx)
|
||||
}
|
||||
|
||||
func (r *queryResolver) ValidateJwtToken(ctx context.Context, params model.ValidateJWTTokenInput) (*model.ValidateJWTTokenResponse, error) {
|
||||
return resolvers.ValidateJwtTokenResolver(ctx, params)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Users(ctx context.Context, params *model.PaginatedInput) (*model.Users, error) {
|
||||
return resolvers.UsersResolver(ctx, params)
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -51,6 +52,8 @@ func AuthorizeHandler() gin.HandlerFunc {
|
||||
gc.JSON(400, gin.H{"error": "invalid response mode"})
|
||||
}
|
||||
|
||||
fmt.Println("=> redirect URI:", redirectURI)
|
||||
fmt.Println("=> state:", state)
|
||||
if redirectURI == "" {
|
||||
redirectURI = "/app"
|
||||
}
|
||||
|
@@ -71,6 +71,10 @@ func OAuthCallbackHandler() gin.HandlerFunc {
|
||||
existingUser, err := db.Provider.GetUserByEmail(user.Email)
|
||||
|
||||
if err != nil {
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) {
|
||||
c.JSON(400, gin.H{"error": "signup is disabled for this instance"})
|
||||
return
|
||||
}
|
||||
// user not registered, register user and generate session token
|
||||
user.SignupMethods = provider
|
||||
// make sure inputRoles don't include protected roles
|
||||
@@ -91,9 +95,12 @@ func OAuthCallbackHandler() gin.HandlerFunc {
|
||||
user.EmailVerifiedAt = &now
|
||||
user, _ = db.Provider.AddUser(user)
|
||||
} else {
|
||||
if user.RevokedTimestamp != nil {
|
||||
c.JSON(400, gin.H{"error": "user access has been revoked"})
|
||||
}
|
||||
|
||||
// user exists in db, check if method was google
|
||||
// if not append google to existing signup method and save it
|
||||
|
||||
signupMethod := existingUser.SignupMethods
|
||||
if !strings.Contains(signupMethod, provider) {
|
||||
signupMethod = signupMethod + "," + provider
|
||||
|
@@ -16,7 +16,11 @@ import (
|
||||
func OAuthLoginHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
hostname := utils.GetHost(c)
|
||||
// deprecating redirectURL instead use redirect_uri
|
||||
redirectURI := strings.TrimSpace(c.Query("redirectURL"))
|
||||
if redirectURI == "" {
|
||||
redirectURI = strings.TrimSpace(c.Query("redirect_uri"))
|
||||
}
|
||||
roles := strings.TrimSpace(c.Query("roles"))
|
||||
state := strings.TrimSpace(c.Query("state"))
|
||||
scopeString := strings.TrimSpace(c.Query("scope"))
|
||||
|
@@ -111,8 +111,6 @@ func TokenHandler() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// rollover the session for security
|
||||
sessionstore.RemoveState(sessionDataSplit[1])
|
||||
// validate session
|
||||
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
|
||||
if err != nil {
|
||||
@@ -122,6 +120,8 @@ func TokenHandler() gin.HandlerFunc {
|
||||
})
|
||||
return
|
||||
}
|
||||
// rollover the session for security
|
||||
sessionstore.RemoveState(sessionDataSplit[1])
|
||||
userID = claims.Subject
|
||||
roles = claims.Roles
|
||||
scope = claims.Scope
|
||||
|
44
server/resolvers/enable_access.go
Normal file
44
server/resolvers/enable_access.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
)
|
||||
|
||||
// EnableAccessResolver is a resolver for enabling user access
|
||||
func EnableAccessResolver(ctx context.Context, params model.UpdateAccessInput) (*model.Response, error) {
|
||||
gc, err := utils.GinContextFromContext(ctx)
|
||||
var res *model.Response
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if !token.IsSuperAdmin(gc) {
|
||||
return res, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
user, err := db.Provider.GetUserByID(params.UserID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
user.RevokedTimestamp = nil
|
||||
|
||||
user, err = db.Provider.UpdateUser(user)
|
||||
if err != nil {
|
||||
log.Println("error updating user:", err)
|
||||
return res, err
|
||||
}
|
||||
|
||||
res = &model.Response{
|
||||
Message: `user access enabled successfully`,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
@@ -54,6 +54,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
||||
disableBasicAuthentication := store.BoolEnv[constants.EnvKeyDisableBasicAuthentication]
|
||||
disableMagicLinkLogin := store.BoolEnv[constants.EnvKeyDisableMagicLinkLogin]
|
||||
disableLoginPage := store.BoolEnv[constants.EnvKeyDisableLoginPage]
|
||||
disableSignUp := store.BoolEnv[constants.EnvKeyDisableSignUp]
|
||||
roles := store.SliceEnv[constants.EnvKeyRoles]
|
||||
defaultRoles := store.SliceEnv[constants.EnvKeyDefaultRoles]
|
||||
protectedRoles := store.SliceEnv[constants.EnvKeyProtectedRoles]
|
||||
@@ -94,6 +95,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
||||
DisableBasicAuthentication: &disableBasicAuthentication,
|
||||
DisableMagicLinkLogin: &disableMagicLinkLogin,
|
||||
DisableLoginPage: &disableLoginPage,
|
||||
DisableSignUp: &disableSignUp,
|
||||
Roles: roles,
|
||||
ProtectedRoles: protectedRoles,
|
||||
DefaultRoles: defaultRoles,
|
||||
|
@@ -43,7 +43,7 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
|
||||
redirectURL := utils.GetAppURL(gc) + "/reset-password"
|
||||
if params.RedirectURI != nil {
|
||||
redirectURL = *params.RedirectURI
|
||||
}
|
||||
|
60
server/resolvers/generate_jwt_keys.go
Normal file
60
server/resolvers/generate_jwt_keys.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
)
|
||||
|
||||
// GenerateJWTKeysResolver mutation to generate new jwt keys
|
||||
func GenerateJWTKeysResolver(ctx context.Context, params model.GenerateJWTKeysInput) (*model.GenerateJWTKeysResponse, error) {
|
||||
gc, err := utils.GinContextFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.IsSuperAdmin(gc) {
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
clientID := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID)
|
||||
if crypto.IsHMACA(params.Type) {
|
||||
secret, _, err := crypto.NewHMACKey(params.Type, clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.GenerateJWTKeysResponse{
|
||||
Secret: &secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if crypto.IsRSA(params.Type) {
|
||||
_, privateKey, publicKey, _, err := crypto.NewRSAKey(params.Type, clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.GenerateJWTKeysResponse{
|
||||
PrivateKey: &privateKey,
|
||||
PublicKey: &publicKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if crypto.IsECDSA(params.Type) {
|
||||
_, privateKey, publicKey, _, err := crypto.NewECDSAKey(params.Type, clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.GenerateJWTKeysResponse{
|
||||
PrivateKey: &privateKey,
|
||||
PublicKey: &publicKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid algorithm")
|
||||
}
|
135
server/resolvers/invite_members.go
Normal file
135
server/resolvers/invite_members.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
emailservice "github.com/authorizerdev/authorizer/server/email"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
)
|
||||
|
||||
// InviteMembersResolver resolver to invite members
|
||||
func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) {
|
||||
gc, err := utils.GinContextFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.IsSuperAdmin(gc) {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
// this feature is only allowed if email server is configured
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
|
||||
return nil, errors.New("email sending is disabled")
|
||||
}
|
||||
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) && envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) {
|
||||
return nil, errors.New("either basic authentication or magic link login is required")
|
||||
}
|
||||
|
||||
// filter valid emails
|
||||
emails := []string{}
|
||||
for _, email := range params.Emails {
|
||||
if utils.IsValidEmail(email) {
|
||||
emails = append(emails, email)
|
||||
}
|
||||
}
|
||||
|
||||
if len(emails) == 0 {
|
||||
return nil, errors.New("no valid emails found")
|
||||
}
|
||||
|
||||
// TODO: optimise to use like query instead of looping through emails and getting user individually
|
||||
// for each emails check if emails exists in db
|
||||
newEmails := []string{}
|
||||
for _, email := range emails {
|
||||
_, err := db.Provider.GetUserByEmail(email)
|
||||
if err != nil {
|
||||
log.Printf("%s user not found. inviting user.", email)
|
||||
newEmails = append(newEmails, email)
|
||||
} else {
|
||||
log.Println("%s user already exists. skipping.", email)
|
||||
}
|
||||
}
|
||||
|
||||
if len(newEmails) == 0 {
|
||||
return nil, errors.New("all emails already exist")
|
||||
}
|
||||
|
||||
// invite new emails
|
||||
for _, email := range newEmails {
|
||||
|
||||
user := models.User{
|
||||
Email: email,
|
||||
Roles: strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ","),
|
||||
}
|
||||
hostname := utils.GetHost(gc)
|
||||
verifyEmailURL := hostname + "/verify_email"
|
||||
appURL := utils.GetAppURL(gc)
|
||||
|
||||
redirectURL := appURL
|
||||
if params.RedirectURI != nil {
|
||||
redirectURL = *params.RedirectURI
|
||||
}
|
||||
|
||||
_, nonceHash, err := utils.GenerateNonce()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
verificationToken, err := token.CreateVerificationToken(email, constants.VerificationTypeForgotPassword, hostname, nonceHash, redirectURL)
|
||||
if err != nil {
|
||||
log.Println(`error generating token`, err)
|
||||
}
|
||||
|
||||
verificationRequest := models.VerificationRequest{
|
||||
Token: verificationToken,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: email,
|
||||
Nonce: nonceHash,
|
||||
RedirectURI: redirectURL,
|
||||
}
|
||||
|
||||
// use magic link login if that option is on
|
||||
if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) {
|
||||
user.SignupMethods = constants.SignupMethodMagicLinkLogin
|
||||
verificationRequest.Identifier = constants.VerificationTypeMagicLinkLogin
|
||||
} else {
|
||||
// use basic authentication if that option is on
|
||||
user.SignupMethods = constants.SignupMethodBasicAuth
|
||||
verificationRequest.Identifier = constants.VerificationTypeForgotPassword
|
||||
|
||||
verifyEmailURL = appURL + "/setup-password"
|
||||
|
||||
}
|
||||
|
||||
user, err = db.Provider.AddUser(user)
|
||||
if err != nil {
|
||||
log.Printf("error inviting user: %s, err: %v", email, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = db.Provider.AddVerificationRequest(verificationRequest)
|
||||
if err != nil {
|
||||
log.Printf("error inviting user: %s, err: %v", email, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go emailservice.InviteEmail(email, verificationToken, verifyEmailURL, redirectURL)
|
||||
}
|
||||
|
||||
return &model.Response{
|
||||
Message: fmt.Sprintf("%d user(s) invited successfully.", len(newEmails)),
|
||||
}, nil
|
||||
}
|
@@ -36,6 +36,10 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
|
||||
return res, fmt.Errorf(`user with this email not found`)
|
||||
}
|
||||
|
||||
if user.RevokedTimestamp != nil {
|
||||
return res, fmt.Errorf(`user access has been revoked`)
|
||||
}
|
||||
|
||||
if !strings.Contains(user.SignupMethods, constants.SignupMethodBasicAuth) {
|
||||
return res, fmt.Errorf(`user has not signed up email & password`)
|
||||
}
|
||||
|
@@ -43,8 +43,11 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
|
||||
|
||||
// find user with email
|
||||
existingUser, err := db.Provider.GetUserByEmail(params.Email)
|
||||
|
||||
if err != nil {
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) {
|
||||
return res, fmt.Errorf(`signup is disabled for this instance`)
|
||||
}
|
||||
|
||||
user.SignupMethods = constants.SignupMethodMagicLinkLogin
|
||||
// define roles for new user
|
||||
if len(params.Roles) > 0 {
|
||||
@@ -67,6 +70,10 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
|
||||
// 2. user has not signed up for one of the available role but trying to signup.
|
||||
// Need to modify roles in this case
|
||||
|
||||
if user.RevokedTimestamp != nil {
|
||||
return res, fmt.Errorf(`user access has been revoked`)
|
||||
}
|
||||
|
||||
// find the unassigned roles
|
||||
if len(params.Roles) <= 0 {
|
||||
inputRoles = envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles)
|
||||
@@ -123,7 +130,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
|
||||
if params.Scope != nil && len(params.Scope) > 0 {
|
||||
redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ")
|
||||
}
|
||||
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
|
||||
redirectURL := utils.GetAppURL(gc)
|
||||
if params.RedirectURI != nil {
|
||||
redirectURL = *params.RedirectURI
|
||||
}
|
||||
|
@@ -35,6 +35,10 @@ func ResetPasswordResolver(ctx context.Context, params model.ResetPasswordInput)
|
||||
return res, fmt.Errorf(`passwords don't match`)
|
||||
}
|
||||
|
||||
if !utils.IsValidPassword(params.Password) {
|
||||
return res, fmt.Errorf(`password is not valid. It needs to be at least 6 characters long and contain at least one number, one uppercase letter, one lowercase letter and one special character`)
|
||||
}
|
||||
|
||||
// verify if token exists in db
|
||||
hostname := utils.GetHost(gc)
|
||||
claim, err := token.ParseJWTToken(params.Token, hostname, verificationRequest.Nonce, verificationRequest.Email)
|
||||
|
49
server/resolvers/revoke_access.go
Normal file
49
server/resolvers/revoke_access.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/sessionstore"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
)
|
||||
|
||||
// RevokeAccessResolver is a resolver for revoking user access
|
||||
func RevokeAccessResolver(ctx context.Context, params model.UpdateAccessInput) (*model.Response, error) {
|
||||
gc, err := utils.GinContextFromContext(ctx)
|
||||
var res *model.Response
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if !token.IsSuperAdmin(gc) {
|
||||
return res, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
user, err := db.Provider.GetUserByID(params.UserID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
user.RevokedTimestamp = &now
|
||||
|
||||
user, err = db.Provider.UpdateUser(user)
|
||||
if err != nil {
|
||||
log.Println("error updating user:", err)
|
||||
return res, err
|
||||
}
|
||||
|
||||
go sessionstore.DeleteAllUserSession(fmt.Sprintf("%x", user.ID))
|
||||
|
||||
res = &model.Response{
|
||||
Message: `user access revoked successfully`,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
@@ -2,7 +2,9 @@ package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/cookie"
|
||||
@@ -25,13 +27,15 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod
|
||||
|
||||
sessionToken, err := cookie.GetSession(gc)
|
||||
if err != nil {
|
||||
return res, err
|
||||
log.Println("error getting session token:", err)
|
||||
return res, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
// get session from cookie
|
||||
claims, err := token.ValidateBrowserSession(gc, sessionToken)
|
||||
if err != nil {
|
||||
return res, err
|
||||
log.Println("session validation failed:", err)
|
||||
return res, errors.New("unauthorized")
|
||||
}
|
||||
userID := claims.Subject
|
||||
user, err := db.Provider.GetUserByID(userID)
|
||||
|
@@ -28,13 +28,22 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
|
||||
return res, err
|
||||
}
|
||||
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) {
|
||||
return res, fmt.Errorf(`signup is disabled for this instance`)
|
||||
}
|
||||
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) {
|
||||
return res, fmt.Errorf(`basic authentication is disabled for this instance`)
|
||||
}
|
||||
|
||||
if params.ConfirmPassword != params.Password {
|
||||
return res, fmt.Errorf(`password and confirm password does not match`)
|
||||
}
|
||||
|
||||
if !utils.IsValidPassword(params.Password) {
|
||||
return res, fmt.Errorf(`password is not valid. It needs to be at least 6 characters long and contain at least one number, one uppercase letter, one lowercase letter and one special character`)
|
||||
}
|
||||
|
||||
params.Email = strings.ToLower(params.Email)
|
||||
|
||||
if !utils.IsValidEmail(params.Email) {
|
||||
@@ -128,7 +137,10 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
|
||||
return res, err
|
||||
}
|
||||
verificationType := constants.VerificationTypeBasicAuthSignup
|
||||
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
|
||||
redirectURL := utils.GetAppURL(gc)
|
||||
if params.RedirectURI != nil {
|
||||
redirectURL = *params.RedirectURI
|
||||
}
|
||||
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL)
|
||||
if err != nil {
|
||||
return res, err
|
||||
|
@@ -53,11 +53,19 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
||||
}
|
||||
|
||||
if isJWTUpdated {
|
||||
// use to reset when type is changed from rsa, edsa -> hmac or vice a versa
|
||||
defaultSecret := ""
|
||||
defaultPublicKey := ""
|
||||
defaultPrivateKey := ""
|
||||
// check if jwt secret is provided
|
||||
if crypto.IsHMACA(algo) {
|
||||
if params.JwtSecret == nil {
|
||||
return res, fmt.Errorf("jwt secret is required for HMAC algorithm")
|
||||
}
|
||||
|
||||
// reset public key and private key
|
||||
params.JwtPrivateKey = &defaultPrivateKey
|
||||
params.JwtPublicKey = &defaultPublicKey
|
||||
}
|
||||
|
||||
if crypto.IsRSA(algo) {
|
||||
@@ -65,6 +73,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
||||
return res, fmt.Errorf("jwt private and public key is required for RSA (PKCS1) / ECDSA algorithm")
|
||||
}
|
||||
|
||||
// reset the jwt secret
|
||||
params.JwtSecret = &defaultSecret
|
||||
_, err = crypto.ParseRsaPrivateKeyFromPemStr(*params.JwtPrivateKey)
|
||||
if err != nil {
|
||||
return res, err
|
||||
@@ -81,6 +91,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
||||
return res, fmt.Errorf("jwt private and public key is required for RSA (PKCS1) / ECDSA algorithm")
|
||||
}
|
||||
|
||||
// reset the jwt secret
|
||||
params.JwtSecret = &defaultSecret
|
||||
_, err = crypto.ParseEcdsaPrivateKeyFromPemStr(*params.JwtPrivateKey)
|
||||
if err != nil {
|
||||
return res, err
|
||||
|
@@ -134,7 +134,7 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
|
||||
return res, err
|
||||
}
|
||||
verificationType := constants.VerificationTypeUpdateEmail
|
||||
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
|
||||
redirectURL := utils.GetAppURL(gc)
|
||||
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
|
||||
if err != nil {
|
||||
log.Println(`error generating token`, err)
|
||||
|
@@ -106,7 +106,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
||||
return res, err
|
||||
}
|
||||
verificationType := constants.VerificationTypeUpdateEmail
|
||||
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
|
||||
redirectURL := utils.GetAppURL(gc)
|
||||
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
|
||||
if err != nil {
|
||||
log.Println(`error generating token`, err)
|
||||
@@ -154,6 +154,8 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
||||
return res, err
|
||||
}
|
||||
|
||||
createdAt := user.CreatedAt
|
||||
updatedAt := user.UpdatedAt
|
||||
res = &model.User{
|
||||
ID: params.ID,
|
||||
Email: user.Email,
|
||||
@@ -161,8 +163,8 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
||||
GivenName: user.GivenName,
|
||||
FamilyName: user.FamilyName,
|
||||
Roles: strings.Split(user.Roles, ","),
|
||||
CreatedAt: &user.CreatedAt,
|
||||
UpdatedAt: &user.UpdatedAt,
|
||||
CreatedAt: &createdAt,
|
||||
UpdatedAt: &updatedAt,
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
86
server/resolvers/validate_jwt_token.go
Normal file
86
server/resolvers/validate_jwt_token.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/sessionstore"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
// ValidateJwtTokenResolver is used to validate a jwt token without its rotation
|
||||
// this can be used at API level (backend)
|
||||
// it can validate:
|
||||
// access_token
|
||||
// id_token
|
||||
// refresh_token
|
||||
func ValidateJwtTokenResolver(ctx context.Context, params model.ValidateJWTTokenInput) (*model.ValidateJWTTokenResponse, error) {
|
||||
gc, err := utils.GinContextFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenType := params.TokenType
|
||||
if tokenType != "access_token" && tokenType != "refresh_token" && tokenType != "id_token" {
|
||||
return nil, errors.New("invalid token type")
|
||||
}
|
||||
|
||||
userID := ""
|
||||
nonce := ""
|
||||
// access_token and refresh_token should be validated from session store as well
|
||||
if tokenType == "access_token" || tokenType == "refresh_token" {
|
||||
savedSession := sessionstore.GetState(params.Token)
|
||||
if savedSession == "" {
|
||||
return &model.ValidateJWTTokenResponse{
|
||||
IsValid: false,
|
||||
}, nil
|
||||
}
|
||||
savedSessionSplit := strings.Split(savedSession, "@")
|
||||
nonce = savedSessionSplit[0]
|
||||
userID = savedSessionSplit[1]
|
||||
}
|
||||
|
||||
hostname := utils.GetHost(gc)
|
||||
var claimRoles []string
|
||||
var claims jwt.MapClaims
|
||||
|
||||
// we cannot validate sub and nonce in case of id_token as that token is not persisted in session store
|
||||
if userID != "" && nonce != "" {
|
||||
claims, err = token.ParseJWTToken(params.Token, hostname, nonce, userID)
|
||||
if err != nil {
|
||||
return &model.ValidateJWTTokenResponse{
|
||||
IsValid: false,
|
||||
}, nil
|
||||
}
|
||||
} else {
|
||||
claims, err = token.ParseJWTTokenWithoutNonce(params.Token, hostname)
|
||||
if err != nil {
|
||||
return &model.ValidateJWTTokenResponse{
|
||||
IsValid: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
claimRolesInterface := claims["roles"]
|
||||
roleSlice := utils.ConvertInterfaceToSlice(claimRolesInterface)
|
||||
for _, v := range roleSlice {
|
||||
claimRoles = append(claimRoles, v.(string))
|
||||
}
|
||||
|
||||
if params.Roles != nil && len(params.Roles) > 0 {
|
||||
for _, v := range params.Roles {
|
||||
if !utils.StringSliceContains(claimRoles, v) {
|
||||
return nil, fmt.Errorf(`unauthorized`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &model.ValidateJWTTokenResponse{
|
||||
IsValid: true,
|
||||
}, nil
|
||||
}
|
@@ -44,6 +44,7 @@ func InitRouter() *gin.Engine {
|
||||
{
|
||||
dashboard.Static("/favicon_io", "dashboard/favicon_io")
|
||||
dashboard.Static("/build", "dashboard/build")
|
||||
dashboard.Static("/public", "dashboard/public")
|
||||
dashboard.GET("/", handlers.DashboardHandler())
|
||||
dashboard.GET("/:page", handlers.DashboardHandler())
|
||||
}
|
||||
|
57
server/test/enable_access_test.go
Normal file
57
server/test/enable_access_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func enableAccessTest(t *testing.T, s TestSetup) {
|
||||
t.Helper()
|
||||
t.Run(`should revoke access`, func(t *testing.T) {
|
||||
req, ctx := createContext(s)
|
||||
email := "revoke_access." + s.TestInfo.Email
|
||||
_, err := resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||
Email: email,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeMagicLinkLogin)
|
||||
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
|
||||
Token: verificationRequest.Token,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, verifyRes.AccessToken)
|
||||
|
||||
h, err := crypto.EncryptPassword(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
||||
assert.Nil(t, err)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
|
||||
|
||||
res, err := resolvers.RevokeAccessResolver(ctx, model.UpdateAccessInput{
|
||||
UserID: verifyRes.User.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, res.Message)
|
||||
|
||||
res, err = resolvers.EnableAccessResolver(ctx, model.UpdateAccessInput{
|
||||
UserID: verifyRes.User.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, res.Message)
|
||||
|
||||
// it should allow login with revoked access
|
||||
res, err = resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||
Email: email,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.NotEmpty(t, res.Message)
|
||||
|
||||
cleanData(email)
|
||||
})
|
||||
}
|
62
server/test/generate_jwt_keys_test.go
Normal file
62
server/test/generate_jwt_keys_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func generateJWTkeyTest(t *testing.T, s TestSetup) {
|
||||
t.Helper()
|
||||
req, ctx := createContext(s)
|
||||
t.Run(`generate_jwt_keys`, func(t *testing.T) {
|
||||
t.Run(`should throw unauthorized`, func(t *testing.T) {
|
||||
res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{
|
||||
Type: "HS256",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
t.Run(`should throw invalid`, func(t *testing.T) {
|
||||
res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{
|
||||
Type: "test",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
h, err := crypto.EncryptPassword(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
||||
assert.Nil(t, err)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
|
||||
t.Run(`should generate HS256 secret`, func(t *testing.T) {
|
||||
res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{
|
||||
Type: "HS256",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, res.Secret)
|
||||
})
|
||||
|
||||
t.Run(`should generate RS256 secret`, func(t *testing.T) {
|
||||
res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{
|
||||
Type: "RS256",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, res.PrivateKey)
|
||||
assert.NotEmpty(t, res.PublicKey)
|
||||
})
|
||||
|
||||
t.Run(`should generate ES256 secret`, func(t *testing.T) {
|
||||
res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{
|
||||
Type: "ES256",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, res.PrivateKey)
|
||||
assert.NotEmpty(t, res.PublicKey)
|
||||
})
|
||||
})
|
||||
}
|
58
server/test/invite_member_test.go
Normal file
58
server/test/invite_member_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func inviteUserTest(t *testing.T, s TestSetup) {
|
||||
t.Helper()
|
||||
t.Run(`should invite user successfully`, func(t *testing.T) {
|
||||
req, ctx := createContext(s)
|
||||
emails := []string{"invite_member1." + s.TestInfo.Email}
|
||||
|
||||
// unauthorized error
|
||||
res, err := resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
|
||||
Emails: emails,
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
h, err := crypto.EncryptPassword(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
||||
assert.Nil(t, err)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
|
||||
|
||||
// invalid emails test
|
||||
invalidEmailsTest := []string{
|
||||
"test",
|
||||
"test.com",
|
||||
}
|
||||
res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
|
||||
Emails: invalidEmailsTest,
|
||||
})
|
||||
|
||||
// valid test
|
||||
res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
|
||||
Emails: emails,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, res)
|
||||
|
||||
// duplicate error test
|
||||
res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
|
||||
Emails: emails,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
cleanData(emails[0])
|
||||
})
|
||||
}
|
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -16,11 +17,17 @@ func magicLinkLoginTests(t *testing.T, s TestSetup) {
|
||||
t.Run(`should login with magic link`, func(t *testing.T) {
|
||||
req, ctx := createContext(s)
|
||||
email := "magic_link_login." + s.TestInfo.Email
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.BoolStoreIdentifier, constants.EnvKeyDisableSignUp, true)
|
||||
_, err := resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||
Email: email,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, err, "signup disabled")
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.BoolStoreIdentifier, constants.EnvKeyDisableSignUp, false)
|
||||
_, err = resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||
Email: email,
|
||||
})
|
||||
assert.Nil(t, err, "signup should be successful")
|
||||
|
||||
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeMagicLinkLogin)
|
||||
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
|
||||
|
@@ -43,6 +43,14 @@ func resetPasswordTest(t *testing.T, s TestSetup) {
|
||||
ConfirmPassword: "test1",
|
||||
})
|
||||
|
||||
assert.NotNil(t, err, "invalid password")
|
||||
|
||||
_, err = resolvers.ResetPasswordResolver(ctx, model.ResetPasswordInput{
|
||||
Token: verificationRequest.Token,
|
||||
Password: "Test@1234",
|
||||
ConfirmPassword: "Test@1234",
|
||||
})
|
||||
|
||||
assert.Nil(t, err, "password changed successfully")
|
||||
|
||||
cleanData(email)
|
||||
|
@@ -48,6 +48,9 @@ func TestResolvers(t *testing.T) {
|
||||
adminSessionTests(t, s)
|
||||
updateEnvTests(t, s)
|
||||
envTests(t, s)
|
||||
revokeAccessTest(t, s)
|
||||
enableAccessTest(t, s)
|
||||
generateJWTkeyTest(t, s)
|
||||
|
||||
// user tests
|
||||
loginTests(t, s)
|
||||
@@ -62,6 +65,8 @@ func TestResolvers(t *testing.T) {
|
||||
magicLinkLoginTests(t, s)
|
||||
logoutTests(t, s)
|
||||
metaTests(t, s)
|
||||
inviteUserTest(t, s)
|
||||
validateJwtTokenTest(t, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
54
server/test/revoke_access_test.go
Normal file
54
server/test/revoke_access_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func revokeAccessTest(t *testing.T, s TestSetup) {
|
||||
t.Helper()
|
||||
t.Run(`should revoke access`, func(t *testing.T) {
|
||||
req, ctx := createContext(s)
|
||||
email := "revoke_access." + s.TestInfo.Email
|
||||
_, err := resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||
Email: email,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeMagicLinkLogin)
|
||||
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
|
||||
Token: verificationRequest.Token,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, verifyRes.AccessToken)
|
||||
|
||||
res, err := resolvers.RevokeAccessResolver(ctx, model.UpdateAccessInput{
|
||||
UserID: verifyRes.User.ID,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
|
||||
h, err := crypto.EncryptPassword(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
||||
assert.Nil(t, err)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
|
||||
|
||||
res, err = resolvers.RevokeAccessResolver(ctx, model.UpdateAccessInput{
|
||||
UserID: verifyRes.User.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, res.Message)
|
||||
|
||||
// it should not allow login with revoked access
|
||||
_, err = resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||
Email: email,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
cleanData(email)
|
||||
})
|
||||
}
|
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -20,14 +21,30 @@ func signupTests(t *testing.T, s TestSetup) {
|
||||
Password: s.TestInfo.Password,
|
||||
ConfirmPassword: s.TestInfo.Password + "s",
|
||||
})
|
||||
assert.NotNil(t, err, "invalid password errors")
|
||||
assert.NotNil(t, err, "invalid password")
|
||||
|
||||
res, err = resolvers.SignupResolver(ctx, model.SignUpInput{
|
||||
Email: email,
|
||||
Password: "test",
|
||||
ConfirmPassword: "test",
|
||||
})
|
||||
assert.NotNil(t, err, "invalid password")
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.BoolStoreIdentifier, constants.EnvKeyDisableSignUp, true)
|
||||
res, err = resolvers.SignupResolver(ctx, model.SignUpInput{
|
||||
Email: email,
|
||||
Password: s.TestInfo.Password,
|
||||
ConfirmPassword: s.TestInfo.Password,
|
||||
})
|
||||
assert.NotNil(t, err, "singup disabled")
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.BoolStoreIdentifier, constants.EnvKeyDisableSignUp, false)
|
||||
res, err = resolvers.SignupResolver(ctx, model.SignUpInput{
|
||||
Email: email,
|
||||
Password: s.TestInfo.Password,
|
||||
ConfirmPassword: s.TestInfo.Password,
|
||||
})
|
||||
assert.Nil(t, err, "signup should be successful")
|
||||
user := *res.User
|
||||
assert.Equal(t, email, user.Email)
|
||||
assert.Nil(t, res.AccessToken, "access token should be nil")
|
||||
|
@@ -68,7 +68,7 @@ func createContext(s TestSetup) (*http.Request, context.Context) {
|
||||
func testSetup() TestSetup {
|
||||
testData := TestData{
|
||||
Email: fmt.Sprintf("%d_authorizer_tester@yopmail.com", time.Now().Unix()),
|
||||
Password: "test",
|
||||
Password: "Test@123",
|
||||
}
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEnvPath, "../../.env.sample")
|
||||
|
90
server/test/validate_jwt_token_test.go
Normal file
90
server/test/validate_jwt_token_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/authorizerdev/authorizer/server/sessionstore"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func validateJwtTokenTest(t *testing.T, s TestSetup) {
|
||||
t.Helper()
|
||||
_, ctx := createContext(s)
|
||||
t.Run(`validate params`, func(t *testing.T) {
|
||||
res, err := resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "access_token",
|
||||
Token: "",
|
||||
})
|
||||
assert.False(t, res.IsValid)
|
||||
res, err = resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "access_token",
|
||||
Token: "invalid",
|
||||
})
|
||||
assert.False(t, res.IsValid)
|
||||
_, err = resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "access_token_invalid",
|
||||
Token: "invalid@invalid",
|
||||
})
|
||||
assert.Error(t, err, "invalid token")
|
||||
})
|
||||
|
||||
scope := []string{"openid", "email", "profile", "offline_access"}
|
||||
user := models.User{
|
||||
ID: uuid.New().String(),
|
||||
Email: "jwt_test_" + s.TestInfo.Email,
|
||||
Roles: "user",
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
roles := []string{"user"}
|
||||
gc, err := utils.GinContextFromContext(ctx)
|
||||
assert.NoError(t, err)
|
||||
authToken, err := token.CreateAuthToken(gc, user, roles, scope)
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
|
||||
t.Run(`should validate the access token`, func(t *testing.T) {
|
||||
res, err := resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "access_token",
|
||||
Token: authToken.AccessToken.Token,
|
||||
Roles: []string{"user"},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.IsValid)
|
||||
|
||||
res, err = resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "access_token",
|
||||
Token: authToken.AccessToken.Token,
|
||||
Roles: []string{"invalid_role"},
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run(`should validate the refresh token`, func(t *testing.T) {
|
||||
res, err := resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "refresh_token",
|
||||
Token: authToken.RefreshToken.Token,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.IsValid)
|
||||
})
|
||||
|
||||
t.Run(`should validate the id token`, func(t *testing.T) {
|
||||
res, err := resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "id_token",
|
||||
Token: authToken.IDToken.Token,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.IsValid)
|
||||
})
|
||||
}
|
@@ -41,3 +41,11 @@ func TestIsValidIdentifier(t *testing.T) {
|
||||
assert.True(t, utils.IsValidVerificationIdentifier(constants.VerificationTypeUpdateEmail), "it should be valid identifier")
|
||||
assert.True(t, utils.IsValidVerificationIdentifier(constants.VerificationTypeForgotPassword), "it should be valid identifier")
|
||||
}
|
||||
|
||||
func TestIsValidPassword(t *testing.T) {
|
||||
assert.False(t, utils.IsValidPassword("test"), "it should be invalid password")
|
||||
assert.False(t, utils.IsValidPassword("Te@1"), "it should be invalid password")
|
||||
assert.False(t, utils.IsValidPassword("n*rp7GGTd29V{xx%{pDb@7n{](SD.!+.Mp#*$EHDGk&$pAMf7e#432Sg,Gr](j3n]jV/3F8BJJT+9u9{q=8zK:8u!rpQBaXJp%A+7r!jQj)M(vC$UX,h;;WKm$U6i#7dBnC&2ryKzKd+(y&=Ud)hErT/j;v3t..CM).8nS)9qLtV7pmP;@2QuzDyGfL7KB()k:BpjAGL@bxD%r5gcBfh7$&wutk!wzMfPFY#nkjjqyZbEHku,{jc;gvbYq2)3w=KExnYz9Vbv:;*;?f##faxkULdMpmm&yEfePixzx+[{[38zGN;3TzF;6M#Xy_tMtx:yK*n$bc(bPyGz%EYkC&]ttUF@#aZ%$QZ:u!icF@+"), "it should be invalid password")
|
||||
assert.False(t, utils.IsValidPassword("test@123"), "it should be invalid password")
|
||||
assert.True(t, utils.IsValidPassword("Test@123"), "it should be valid password")
|
||||
}
|
||||
|
@@ -165,7 +165,12 @@ func GetAccessToken(gc *gin.Context) (string, error) {
|
||||
return "", fmt.Errorf(`unauthorized`)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
authSplit := strings.Split(auth, " ")
|
||||
if len(authSplit) != 2 {
|
||||
return "", fmt.Errorf(`unauthorized`)
|
||||
}
|
||||
|
||||
if strings.ToLower(authSplit[0]) != "bearer" {
|
||||
return "", fmt.Errorf(`not a bearer token`)
|
||||
}
|
||||
|
||||
@@ -358,7 +363,12 @@ func GetIDToken(gc *gin.Context) (string, error) {
|
||||
return "", fmt.Errorf(`unauthorized`)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
authSplit := strings.Split(auth, " ")
|
||||
if len(authSplit) != 2 {
|
||||
return "", fmt.Errorf(`unauthorized`)
|
||||
}
|
||||
|
||||
if strings.ToLower(authSplit[0]) != "bearer" {
|
||||
return "", fmt.Errorf(`not a bearer token`)
|
||||
}
|
||||
|
||||
|
@@ -105,3 +105,59 @@ func ParseJWTToken(token, hostname, nonce, subject string) (jwt.MapClaims, error
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// ParseJWTTokenWithoutNonce common util to parse jwt token without nonce
|
||||
// used to validate ID token as it is not persisted in store
|
||||
func ParseJWTTokenWithoutNonce(token, hostname string) (jwt.MapClaims, error) {
|
||||
jwtType := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtType)
|
||||
signingMethod := jwt.GetSigningMethod(jwtType)
|
||||
|
||||
var err error
|
||||
var claims jwt.MapClaims
|
||||
|
||||
switch signingMethod {
|
||||
case jwt.SigningMethodHS256, jwt.SigningMethodHS384, jwt.SigningMethodHS512:
|
||||
_, err = jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtSecret)), nil
|
||||
})
|
||||
case jwt.SigningMethodRS256, jwt.SigningMethodRS384, jwt.SigningMethodRS512:
|
||||
_, err = jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
key, err := crypto.ParseRsaPublicKeyFromPemStr(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
})
|
||||
case jwt.SigningMethodES256, jwt.SigningMethodES384, jwt.SigningMethodES512:
|
||||
_, err = jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
key, err := crypto.ParseEcdsaPublicKeyFromPemStr(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
})
|
||||
default:
|
||||
err = errors.New("unsupported signing method")
|
||||
}
|
||||
if err != nil {
|
||||
return claims, err
|
||||
}
|
||||
|
||||
// claim parses exp & iat into float 64 with e^10,
|
||||
// but we expect it to be int64
|
||||
// hence we need to assert interface and convert to int64
|
||||
intExp := int64(claims["exp"].(float64))
|
||||
intIat := int64(claims["iat"].(float64))
|
||||
claims["exp"] = intExp
|
||||
claims["iat"] = intIat
|
||||
|
||||
if claims["aud"] != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) {
|
||||
return claims, errors.New("invalid audience")
|
||||
}
|
||||
|
||||
if claims["iss"] != hostname {
|
||||
return claims, errors.New("invalid issuer")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"reflect"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
@@ -47,3 +48,24 @@ func RemoveDuplicateString(strSlice []string) []string {
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// ConvertInterfaceToSlice to convert interface to slice interface
|
||||
func ConvertInterfaceToSlice(slice interface{}) []interface{} {
|
||||
s := reflect.ValueOf(slice)
|
||||
if s.Kind() != reflect.Slice {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Keep the distinction between nil and empty slice input
|
||||
if s.IsNil() {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := make([]interface{}, s.Len())
|
||||
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
ret[i] = s.Index(i).Interface()
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
@@ -17,5 +17,6 @@ func GetMetaInfo() model.Meta {
|
||||
IsBasicAuthenticationEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication),
|
||||
IsEmailVerificationEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification),
|
||||
IsMagicLinkLoginEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin),
|
||||
IsSignUpEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp),
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -71,3 +73,12 @@ func GetDomainName(uri string) string {
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
// GetAppURL to get /app/ url if not configured by user
|
||||
func GetAppURL(gc *gin.Context) string {
|
||||
envAppURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
|
||||
if envAppURL == "" {
|
||||
envAppURL = GetHost(gc) + "/app"
|
||||
}
|
||||
return envAppURL
|
||||
}
|
||||
|
@@ -86,3 +86,35 @@ func IsStringArrayEqual(a, b []string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ValidatePassword to validate the password against the following policy
|
||||
// min char length: 6
|
||||
// max char length: 36
|
||||
// at least one upper case letter
|
||||
// at least one lower case letter
|
||||
// at least one digit
|
||||
// at least one special character
|
||||
func IsValidPassword(password string) bool {
|
||||
if len(password) < 6 || len(password) > 36 {
|
||||
return false
|
||||
}
|
||||
|
||||
hasUpperCase := false
|
||||
hasLowerCase := false
|
||||
hasDigit := false
|
||||
hasSpecialChar := false
|
||||
|
||||
for _, char := range password {
|
||||
if char >= 'A' && char <= 'Z' {
|
||||
hasUpperCase = true
|
||||
} else if char >= 'a' && char <= 'z' {
|
||||
hasLowerCase = true
|
||||
} else if char >= '0' && char <= '9' {
|
||||
hasDigit = true
|
||||
} else {
|
||||
hasSpecialChar = true
|
||||
}
|
||||
}
|
||||
|
||||
return hasUpperCase && hasLowerCase && hasDigit && hasSpecialChar
|
||||
}
|
||||
|
Reference in New Issue
Block a user