feat: use multi roles login (#60)

* feat: use multi roles login

- add support for protected roles
- refactor oauth code

* fix: adminUpdate role validation

* fix: update app
This commit is contained in:
Lakhan Samani 2021-10-13 22:11:41 +05:30 committed by GitHub
parent 27944cf7b5
commit b376ee3b73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 248 additions and 219 deletions

View File

@ -5,6 +5,7 @@ ADMIN_SECRET=admin
DISABLE_EMAIL_VERIFICATION=true DISABLE_EMAIL_VERIFICATION=true
JWT_SECRET=random_string JWT_SECRET=random_string
JWT_TYPE=HS256 JWT_TYPE=HS256
ROLES=user,admin ROLES=user
DEFAULT_ROLE=user DEFAULT_ROLES=user
PROTECTED_ROLES=admin
JWT_ROLE_CLAIM=role JWT_ROLE_CLAIM=role

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

30
app/package-lock.json generated
View File

@ -8,7 +8,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^0.1.0-beta.18", "@authorizerdev/authorizer-react": "^0.1.0-beta.19",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17", "esbuild": "^0.12.17",
@ -22,9 +22,9 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-js": { "node_modules/@authorizerdev/authorizer-js": {
"version": "0.1.0-beta.19", "version": "0.1.0-beta.22",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.1.0-beta.19.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.1.0-beta.22.tgz",
"integrity": "sha512-//uYjklwQfQKqLJHMAyjdrzh2nz6DycB3lEgl6bTXxmSbrz+l1kQyxB3y8wP/W30IrBQz8bZb+1sau+LD/FU7g==", "integrity": "sha512-fHyZDL49eEsbxxIb2xxW6iLM+/N60m5e2NeVVlMFZjZFX9eUetdFatCOrPTuukGul7l8wO6YofTnwqOLOVjKrA==",
"dependencies": { "dependencies": {
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
}, },
@ -33,11 +33,11 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-react": { "node_modules/@authorizerdev/authorizer-react": {
"version": "0.1.0-beta.18", "version": "0.1.0-beta.19",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.1.0-beta.18.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.1.0-beta.19.tgz",
"integrity": "sha512-lRWWlS9akZwwINRW1NatsbMob06NXht3HXNTUTlu1s8m1YjxmFRE/AL6UIplzAYTpR6eDWMxEEaS0qAVxovUcg==", "integrity": "sha512-ScAG3Auu0KirxuxpQ25JGtYCDmiC/3DM/ngnGO+XBaW+bWUARifH2HPkOnddUxkKPAfrKyxoa/l730W8mxhy+A==",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": "^0.1.0-beta.19", "@authorizerdev/authorizer-js": "^0.1.0-beta.22",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"
@ -797,19 +797,19 @@
}, },
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": { "@authorizerdev/authorizer-js": {
"version": "0.1.0-beta.19", "version": "0.1.0-beta.22",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.1.0-beta.19.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.1.0-beta.22.tgz",
"integrity": "sha512-//uYjklwQfQKqLJHMAyjdrzh2nz6DycB3lEgl6bTXxmSbrz+l1kQyxB3y8wP/W30IrBQz8bZb+1sau+LD/FU7g==", "integrity": "sha512-fHyZDL49eEsbxxIb2xxW6iLM+/N60m5e2NeVVlMFZjZFX9eUetdFatCOrPTuukGul7l8wO6YofTnwqOLOVjKrA==",
"requires": { "requires": {
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
} }
}, },
"@authorizerdev/authorizer-react": { "@authorizerdev/authorizer-react": {
"version": "0.1.0-beta.18", "version": "0.1.0-beta.19",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.1.0-beta.18.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.1.0-beta.19.tgz",
"integrity": "sha512-lRWWlS9akZwwINRW1NatsbMob06NXht3HXNTUTlu1s8m1YjxmFRE/AL6UIplzAYTpR6eDWMxEEaS0qAVxovUcg==", "integrity": "sha512-ScAG3Auu0KirxuxpQ25JGtYCDmiC/3DM/ngnGO+XBaW+bWUARifH2HPkOnddUxkKPAfrKyxoa/l730W8mxhy+A==",
"requires": { "requires": {
"@authorizerdev/authorizer-js": "^0.1.0-beta.19", "@authorizerdev/authorizer-js": "^0.1.0-beta.22",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"

View File

@ -10,7 +10,7 @@
"author": "Lakhan Samani", "author": "Lakhan Samani",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "^0.1.0-beta.18", "@authorizerdev/authorizer-react": "^0.1.0-beta.19",
"@types/react": "^17.0.15", "@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17", "esbuild": "^0.12.17",

View File

@ -24,7 +24,8 @@ var (
// ROLES // ROLES
ROLES = []string{} ROLES = []string{}
DEFAULT_ROLE = "" PROTECTED_ROLES = []string{}
DEFAULT_ROLES = []string{}
JWT_ROLE_CLAIM = "role" JWT_ROLE_CLAIM = "role"
// OAuth login // OAuth login

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/utils"
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
@ -63,7 +64,6 @@ func InitEnv() {
constants.RESET_PASSWORD_URL = strings.TrimPrefix(os.Getenv("RESET_PASSWORD_URL"), "/") constants.RESET_PASSWORD_URL = strings.TrimPrefix(os.Getenv("RESET_PASSWORD_URL"), "/")
constants.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION") constants.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION")
constants.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION") constants.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION")
constants.DEFAULT_ROLE = os.Getenv("DEFAULT_ROLE")
constants.JWT_ROLE_CLAIM = os.Getenv("JWT_ROLE_CLAIM") constants.JWT_ROLE_CLAIM = os.Getenv("JWT_ROLE_CLAIM")
if constants.ADMIN_SECRET == "" { if constants.ADMIN_SECRET == "" {
@ -136,7 +136,26 @@ func InitEnv() {
rolesSplit := strings.Split(os.Getenv("ROLES"), ",") rolesSplit := strings.Split(os.Getenv("ROLES"), ",")
roles := []string{} roles := []string{}
defaultRole := "" if len(rolesSplit) == 0 {
roles = []string{"user"}
}
defaultRoleSplit := strings.Split(os.Getenv("DEFAULT_ROLES"), ",")
defaultRoles := []string{}
if len(defaultRoleSplit) == 0 {
defaultRoles = []string{"user"}
}
protectedRolesSplit := strings.Split(os.Getenv("PROTECTED_ROLES"), ",")
protectedRoles := []string{}
if len(protectedRolesSplit) > 0 {
for _, val := range protectedRolesSplit {
trimVal := strings.TrimSpace(val)
protectedRoles = append(protectedRoles, trimVal)
}
}
for _, val := range rolesSplit { for _, val := range rolesSplit {
trimVal := strings.TrimSpace(val) trimVal := strings.TrimSpace(val)
@ -144,20 +163,18 @@ func InitEnv() {
roles = append(roles, trimVal) roles = append(roles, trimVal)
} }
if trimVal == constants.DEFAULT_ROLE { if utils.StringContains(defaultRoleSplit, trimVal) {
defaultRole = trimVal defaultRoles = append(defaultRoles, trimVal)
} }
} }
if len(roles) > 0 && defaultRole == "" {
if len(roles) > 0 && len(defaultRoles) == 0 && len(defaultRoleSplit) > 0 {
panic(`Invalid DEFAULT_ROLE environment variable. It can be one from give ROLES environment variable value`) panic(`Invalid DEFAULT_ROLE environment variable. It can be one from give ROLES environment variable value`)
} }
if len(roles) == 0 {
roles = []string{"user", "admin"}
constants.DEFAULT_ROLE = "user"
}
constants.ROLES = roles constants.ROLES = roles
constants.DEFAULT_ROLES = defaultRoles
constants.PROTECTED_ROLES = protectedRoles
if constants.JWT_ROLE_CLAIM == "" { if constants.JWT_ROLE_CLAIM == "" {
constants.JWT_ROLE_CLAIM = "role" constants.JWT_ROLE_CLAIM = "role"

View File

@ -81,7 +81,7 @@ type ComplexityRoot struct {
Query struct { Query struct {
Meta func(childComplexity int) int Meta func(childComplexity int) int
Profile func(childComplexity int) int Profile func(childComplexity int) int
Token func(childComplexity int, role *string) int Token func(childComplexity int, roles []string) int
Users func(childComplexity int) int Users func(childComplexity int) int
VerificationRequests func(childComplexity int) int VerificationRequests func(childComplexity int) int
} }
@ -129,7 +129,7 @@ type MutationResolver interface {
type QueryResolver interface { type QueryResolver interface {
Meta(ctx context.Context) (*model.Meta, error) Meta(ctx context.Context) (*model.Meta, error)
Users(ctx context.Context) ([]*model.User, error) Users(ctx context.Context) ([]*model.User, error)
Token(ctx context.Context, role *string) (*model.AuthResponse, error) Token(ctx context.Context, roles []string) (*model.AuthResponse, error)
Profile(ctx context.Context) (*model.User, error) Profile(ctx context.Context) (*model.User, error)
VerificationRequests(ctx context.Context) ([]*model.VerificationRequest, error) VerificationRequests(ctx context.Context) ([]*model.VerificationRequest, error)
} }
@ -379,7 +379,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false return 0, false
} }
return e.complexity.Query.Token(childComplexity, args["role"].(*string)), true return e.complexity.Query.Token(childComplexity, args["roles"].([]string)), true
case "Query.users": case "Query.users":
if e.complexity.Query.Users == nil { if e.complexity.Query.Users == nil {
@ -648,13 +648,13 @@ input SignUpInput {
password: String! password: String!
confirmPassword: String! confirmPassword: String!
image: String image: String
roles: [String] roles: [String!]
} }
input LoginInput { input LoginInput {
email: String! email: String!
password: String! password: String!
role: String roles: [String!]
} }
input VerifyEmailInput { input VerifyEmailInput {
@ -715,7 +715,7 @@ type Mutation {
type Query { type Query {
meta: Meta! meta: Meta!
users: [User!]! users: [User!]!
token(role: String): AuthResponse token(roles: [String!]): AuthResponse
profile: User! profile: User!
verificationRequests: [VerificationRequest!]! verificationRequests: [VerificationRequest!]!
} }
@ -880,15 +880,15 @@ func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs
func (ec *executionContext) field_Query_token_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Query_token_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
var arg0 *string var arg0 []string
if tmp, ok := rawArgs["role"]; ok { if tmp, ok := rawArgs["roles"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("role")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("roles"))
arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp) arg0, err = ec.unmarshalOString2ᚕstringᚄ(ctx, tmp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
args["role"] = arg0 args["roles"] = arg0
return args, nil return args, nil
} }
@ -1884,7 +1884,7 @@ func (ec *executionContext) _Query_token(ctx context.Context, field graphql.Coll
fc.Args = args fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().Token(rctx, args["role"].(*string)) return ec.resolvers.Query().Token(rctx, args["roles"].([]string))
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@ -3842,11 +3842,11 @@ func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj in
if err != nil { if err != nil {
return it, err return it, err
} }
case "role": case "roles":
var err error var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("role")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("roles"))
it.Role, err = ec.unmarshalOString2ᚖstring(ctx, v) it.Roles, err = ec.unmarshalOString2ᚕstringᚄ(ctx, v)
if err != nil { if err != nil {
return it, err return it, err
} }
@ -3970,7 +3970,7 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i
var err error var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("roles")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("roles"))
it.Roles, err = ec.unmarshalOString2ᚕstring(ctx, v) it.Roles, err = ec.unmarshalOString2ᚕstring(ctx, v)
if err != nil { if err != nil {
return it, err return it, err
} }
@ -5280,6 +5280,42 @@ func (ec *executionContext) marshalOString2string(ctx context.Context, sel ast.S
return graphql.MarshalString(v) return graphql.MarshalString(v)
} }
func (ec *executionContext) unmarshalOString2ᚕstringᚄ(ctx context.Context, v interface{}) ([]string, error) {
if v == nil {
return nil, nil
}
var vSlice []interface{}
if v != nil {
if tmp1, ok := v.([]interface{}); ok {
vSlice = tmp1
} else {
vSlice = []interface{}{v}
}
}
var err error
res := make([]string, len(vSlice))
for i := range vSlice {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i))
res[i], err = ec.unmarshalNString2string(ctx, vSlice[i])
if err != nil {
return nil, err
}
}
return res, nil
}
func (ec *executionContext) marshalOString2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler {
if v == nil {
return graphql.Null
}
ret := make(graphql.Array, len(v))
for i := range v {
ret[i] = ec.marshalNString2string(ctx, sel, v[i])
}
return ret
}
func (ec *executionContext) unmarshalOString2ᚕᚖstring(ctx context.Context, v interface{}) ([]*string, error) { func (ec *executionContext) unmarshalOString2ᚕᚖstring(ctx context.Context, v interface{}) ([]*string, error) {
if v == nil { if v == nil {
return nil, nil return nil, nil

View File

@ -34,7 +34,7 @@ type ForgotPasswordInput struct {
type LoginInput struct { type LoginInput struct {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
Role *string `json:"role"` Roles []string `json:"roles"`
} }
type Meta struct { type Meta struct {
@ -68,7 +68,7 @@ type SignUpInput struct {
Password string `json:"password"` Password string `json:"password"`
ConfirmPassword string `json:"confirmPassword"` ConfirmPassword string `json:"confirmPassword"`
Image *string `json:"image"` Image *string `json:"image"`
Roles []*string `json:"roles"` Roles []string `json:"roles"`
} }
type UpdateProfileInput struct { type UpdateProfileInput struct {

View File

@ -61,13 +61,13 @@ input SignUpInput {
password: String! password: String!
confirmPassword: String! confirmPassword: String!
image: String image: String
roles: [String] roles: [String!]
} }
input LoginInput { input LoginInput {
email: String! email: String!
password: String! password: String!
role: String roles: [String!]
} }
input VerifyEmailInput { input VerifyEmailInput {
@ -128,7 +128,7 @@ type Mutation {
type Query { type Query {
meta: Meta! meta: Meta!
users: [User!]! users: [User!]!
token(role: String): AuthResponse token(roles: [String!]): AuthResponse
profile: User! profile: User!
verificationRequests: [VerificationRequest!]! verificationRequests: [VerificationRequest!]!
} }

View File

@ -59,8 +59,8 @@ func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
return resolvers.Users(ctx) return resolvers.Users(ctx)
} }
func (r *queryResolver) Token(ctx context.Context, role *string) (*model.AuthResponse, error) { func (r *queryResolver) Token(ctx context.Context, roles []string) (*model.AuthResponse, error) {
return resolvers.Token(ctx, role) return resolvers.Token(ctx, roles)
} }
func (r *queryResolver) Profile(ctx context.Context) (*model.User, error) { func (r *queryResolver) Profile(ctx context.Context) (*model.User, error) {

View File

@ -19,28 +19,29 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
func processGoogleUserInfo(code string, role string, c *gin.Context) error { func processGoogleUserInfo(code string, roles []string, c *gin.Context) (db.User, error) {
user := db.User{}
token, err := oauth.OAuthProvider.GoogleConfig.Exchange(oauth2.NoContext, code) token, err := oauth.OAuthProvider.GoogleConfig.Exchange(oauth2.NoContext, code)
if err != nil { if err != nil {
return fmt.Errorf("invalid google exchange code: %s", err.Error()) return user, fmt.Errorf("invalid google exchange code: %s", err.Error())
} }
client := oauth.OAuthProvider.GoogleConfig.Client(oauth2.NoContext, token) client := oauth.OAuthProvider.GoogleConfig.Client(oauth2.NoContext, token)
response, err := client.Get(constants.GoogleUserInfoURL) response, err := client.Get(constants.GoogleUserInfoURL)
if err != nil { if err != nil {
return err return user, err
} }
defer response.Body.Close() defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body) body, err := ioutil.ReadAll(response.Body)
if err != nil { if err != nil {
return fmt.Errorf("failed to read google response body: %s", err.Error()) return user, fmt.Errorf("failed to read google response body: %s", err.Error())
} }
userRawData := make(map[string]string) userRawData := make(map[string]string)
json.Unmarshal(body, &userRawData) json.Unmarshal(body, &userRawData)
existingUser, err := db.Mgr.GetUserByEmail(userRawData["email"]) existingUser, err := db.Mgr.GetUserByEmail(userRawData["email"])
user := db.User{ user = db.User{
FirstName: userRawData["given_name"], FirstName: userRawData["given_name"],
LastName: userRawData["family_name"], LastName: userRawData["family_name"],
Image: userRawData["picture"], Image: userRawData["picture"],
@ -50,7 +51,7 @@ func processGoogleUserInfo(code string, role string, c *gin.Context) error {
if err != nil { if err != nil {
// user not registered, register user and generate session token // user not registered, register user and generate session token
user.SignupMethod = enum.Google.String() user.SignupMethod = enum.Google.String()
user.Roles = role user.Roles = strings.Join(roles, ",")
} else { } else {
// user exists in db, check if method was google // user exists in db, check if method was google
// if not append google to existing signup method and save it // if not append google to existing signup method and save it
@ -61,34 +62,25 @@ func processGoogleUserInfo(code string, role string, c *gin.Context) error {
} }
user.SignupMethod = signupMethod user.SignupMethod = signupMethod
user.Password = existingUser.Password user.Password = existingUser.Password
if !utils.IsValidRole(strings.Split(existingUser.Roles, ","), role) { if !utils.IsValidRoles(strings.Split(existingUser.Roles, ","), roles) {
return fmt.Errorf("invalid role") return user, fmt.Errorf("invalid role")
} }
user.Roles = existingUser.Roles user.Roles = existingUser.Roles
} }
return user, nil
user, _ = db.Mgr.SaveUser(user)
user, _ = db.Mgr.GetUserByEmail(user.Email)
userIdStr := fmt.Sprintf("%v", user.ID)
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, role)
accessToken, _, _ := utils.CreateAuthToken(user, enum.AccessToken, role)
utils.SetCookie(c, accessToken)
session.SetToken(userIdStr, refreshToken)
return nil
} }
func processGithubUserInfo(code string, role string, c *gin.Context) error { func processGithubUserInfo(code string, roles []string, c *gin.Context) (db.User, error) {
user := db.User{}
token, err := oauth.OAuthProvider.GithubConfig.Exchange(oauth2.NoContext, code) token, err := oauth.OAuthProvider.GithubConfig.Exchange(oauth2.NoContext, code)
if err != nil { if err != nil {
return fmt.Errorf("invalid github exchange code: %s", err.Error()) return user, fmt.Errorf("invalid github exchange code: %s", err.Error())
} }
client := http.Client{} client := http.Client{}
req, err := http.NewRequest("GET", constants.GithubUserInfoURL, nil) req, err := http.NewRequest("GET", constants.GithubUserInfoURL, nil)
if err != nil { if err != nil {
return fmt.Errorf("error creating github user info request: %s", err.Error()) return user, fmt.Errorf("error creating github user info request: %s", err.Error())
} }
req.Header = http.Header{ req.Header = http.Header{
"Authorization": []string{fmt.Sprintf("token %s", token.AccessToken)}, "Authorization": []string{fmt.Sprintf("token %s", token.AccessToken)},
@ -96,13 +88,13 @@ func processGithubUserInfo(code string, role string, c *gin.Context) error {
response, err := client.Do(req) response, err := client.Do(req)
if err != nil { if err != nil {
return err return user, err
} }
defer response.Body.Close() defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body) body, err := ioutil.ReadAll(response.Body)
if err != nil { if err != nil {
return fmt.Errorf("failed to read github response body: %s", err.Error()) return user, fmt.Errorf("failed to read github response body: %s", err.Error())
} }
userRawData := make(map[string]string) userRawData := make(map[string]string)
@ -118,7 +110,7 @@ func processGithubUserInfo(code string, role string, c *gin.Context) error {
if len(name) > 1 && strings.TrimSpace(name[1]) != "" { if len(name) > 1 && strings.TrimSpace(name[1]) != "" {
lastName = name[0] lastName = name[0]
} }
user := db.User{ user = db.User{
FirstName: firstName, FirstName: firstName,
LastName: lastName, LastName: lastName,
Image: userRawData["avatar_url"], Image: userRawData["avatar_url"],
@ -128,7 +120,7 @@ func processGithubUserInfo(code string, role string, c *gin.Context) error {
if err != nil { if err != nil {
// user not registered, register user and generate session token // user not registered, register user and generate session token
user.SignupMethod = enum.Github.String() user.SignupMethod = enum.Github.String()
user.Roles = role user.Roles = strings.Join(roles, ",")
} else { } else {
// user exists in db, check if method was google // user exists in db, check if method was google
// if not append google to existing signup method and save it // if not append google to existing signup method and save it
@ -140,45 +132,38 @@ func processGithubUserInfo(code string, role string, c *gin.Context) error {
user.SignupMethod = signupMethod user.SignupMethod = signupMethod
user.Password = existingUser.Password user.Password = existingUser.Password
if !utils.IsValidRole(strings.Split(existingUser.Roles, ","), role) { if !utils.IsValidRoles(strings.Split(existingUser.Roles, ","), roles) {
return fmt.Errorf("invalid role") return user, fmt.Errorf("invalid role")
} }
user.Roles = existingUser.Roles user.Roles = existingUser.Roles
} }
user, _ = db.Mgr.SaveUser(user) return user, nil
user, _ = db.Mgr.GetUserByEmail(user.Email)
userIdStr := fmt.Sprintf("%v", user.ID)
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, role)
accessToken, _, _ := utils.CreateAuthToken(user, enum.AccessToken, role)
utils.SetCookie(c, accessToken)
session.SetToken(userIdStr, refreshToken)
return nil
} }
func processFacebookUserInfo(code string, role string, c *gin.Context) error { func processFacebookUserInfo(code string, roles []string, c *gin.Context) (db.User, error) {
user := db.User{}
token, err := oauth.OAuthProvider.FacebookConfig.Exchange(oauth2.NoContext, code) token, err := oauth.OAuthProvider.FacebookConfig.Exchange(oauth2.NoContext, code)
if err != nil { if err != nil {
return fmt.Errorf("invalid facebook exchange code: %s", err.Error()) return user, fmt.Errorf("invalid facebook exchange code: %s", err.Error())
} }
client := http.Client{} client := http.Client{}
req, err := http.NewRequest("GET", constants.FacebookUserInfoURL+token.AccessToken, nil) req, err := http.NewRequest("GET", constants.FacebookUserInfoURL+token.AccessToken, nil)
if err != nil { if err != nil {
return fmt.Errorf("error creating facebook user info request: %s", err.Error()) return user, fmt.Errorf("error creating facebook user info request: %s", err.Error())
} }
response, err := client.Do(req) response, err := client.Do(req)
if err != nil { if err != nil {
log.Println("err:", err) log.Println("err:", err)
return err return user, err
} }
defer response.Body.Close() defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body) body, err := ioutil.ReadAll(response.Body)
if err != nil { if err != nil {
return fmt.Errorf("failed to read facebook response body: %s", err.Error()) return user, fmt.Errorf("failed to read facebook response body: %s", err.Error())
} }
userRawData := make(map[string]interface{}) userRawData := make(map[string]interface{})
@ -189,7 +174,7 @@ func processFacebookUserInfo(code string, role string, c *gin.Context) error {
picObject := userRawData["picture"].(map[string]interface{})["data"] picObject := userRawData["picture"].(map[string]interface{})["data"]
picDataObject := picObject.(map[string]interface{}) picDataObject := picObject.(map[string]interface{})
user := db.User{ user = db.User{
FirstName: fmt.Sprintf("%v", userRawData["first_name"]), FirstName: fmt.Sprintf("%v", userRawData["first_name"]),
LastName: fmt.Sprintf("%v", userRawData["last_name"]), LastName: fmt.Sprintf("%v", userRawData["last_name"]),
Image: fmt.Sprintf("%v", picDataObject["url"]), Image: fmt.Sprintf("%v", picDataObject["url"]),
@ -200,7 +185,7 @@ func processFacebookUserInfo(code string, role string, c *gin.Context) error {
if err != nil { if err != nil {
// user not registered, register user and generate session token // user not registered, register user and generate session token
user.SignupMethod = enum.Github.String() user.SignupMethod = enum.Github.String()
user.Roles = role user.Roles = strings.Join(roles, ",")
} else { } else {
// user exists in db, check if method was google // user exists in db, check if method was google
// if not append google to existing signup method and save it // if not append google to existing signup method and save it
@ -212,22 +197,14 @@ func processFacebookUserInfo(code string, role string, c *gin.Context) error {
user.SignupMethod = signupMethod user.SignupMethod = signupMethod
user.Password = existingUser.Password user.Password = existingUser.Password
if !utils.IsValidRole(strings.Split(existingUser.Roles, ","), role) { if !utils.IsValidRoles(strings.Split(existingUser.Roles, ","), roles) {
return fmt.Errorf("invalid role") return user, fmt.Errorf("invalid role")
} }
user.Roles = existingUser.Roles user.Roles = existingUser.Roles
} }
user, _ = db.Mgr.SaveUser(user) return user, nil
user, _ = db.Mgr.GetUserByEmail(user.Email)
userIdStr := fmt.Sprintf("%v", user.ID)
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, role)
accessToken, _, _ := utils.CreateAuthToken(user, enum.AccessToken, role)
utils.SetCookie(c, accessToken)
session.SetToken(userIdStr, refreshToken)
return nil
} }
func OAuthCallbackHandler() gin.HandlerFunc { func OAuthCallbackHandler() gin.HandlerFunc {
@ -249,18 +226,19 @@ func OAuthCallbackHandler() gin.HandlerFunc {
return return
} }
role := sessionSplit[2] roles := strings.Split(sessionSplit[2], ",")
redirectURL := sessionSplit[1] redirectURL := sessionSplit[1]
var err error var err error
user := db.User{}
code := c.Request.FormValue("code") code := c.Request.FormValue("code")
switch provider { switch provider {
case enum.Google.String(): case enum.Google.String():
err = processGoogleUserInfo(code, role, c) user, err = processGoogleUserInfo(code, roles, c)
case enum.Github.String(): case enum.Github.String():
err = processGithubUserInfo(code, role, c) user, err = processGithubUserInfo(code, roles, c)
case enum.Facebook.String(): case enum.Facebook.String():
err = processFacebookUserInfo(code, role, c) user, err = processFacebookUserInfo(code, roles, c)
default: default:
err = fmt.Errorf(`invalid oauth provider`) err = fmt.Errorf(`invalid oauth provider`)
} }
@ -269,6 +247,16 @@ func OAuthCallbackHandler() gin.HandlerFunc {
c.JSON(400, gin.H{"error": err.Error()}) c.JSON(400, gin.H{"error": err.Error()})
return return
} }
user, _ = db.Mgr.SaveUser(user)
user, _ = db.Mgr.GetUserByEmail(user.Email)
userIdStr := fmt.Sprintf("%v", user.ID)
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, roles)
accessToken, _, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
utils.SetCookie(c, accessToken)
session.SetToken(userIdStr, refreshToken)
c.Redirect(http.StatusTemporaryRedirect, redirectURL) c.Redirect(http.StatusTemporaryRedirect, redirectURL)
} }
} }

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"net/http" "net/http"
"strings"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/enum" "github.com/authorizerdev/authorizer/server/enum"
@ -18,7 +19,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// TODO validate redirect URL // TODO validate redirect URL
redirectURL := c.Query("redirectURL") redirectURL := c.Query("redirectURL")
role := c.Query("role") roles := c.Query("roles")
if redirectURL == "" { if redirectURL == "" {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
@ -27,20 +28,24 @@ func OAuthLoginHandler() gin.HandlerFunc {
return return
} }
if role != "" { if roles != "" {
// validate role // validate role
if !utils.IsValidRole(constants.ROLES, role) { rolesSplit := strings.Split(roles, ",")
// use protected roles verification for admin login only.
// though if not associated with user, it will be rejected from oauth_callback
if !utils.IsValidRoles(append([]string{}, append(constants.ROLES, constants.PROTECTED_ROLES...)...), rolesSplit) {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"error": "invalid role", "error": "invalid role",
}) })
return return
} }
} else { } else {
role = constants.DEFAULT_ROLE roles = strings.Join(constants.DEFAULT_ROLES, ",")
} }
uuid := uuid.New() uuid := uuid.New()
oauthStateString := uuid.String() + "___" + redirectURL + "___" + role oauthStateString := uuid.String() + "___" + redirectURL + "___" + roles
provider := c.Param("oauth_provider") provider := c.Param("oauth_provider")

View File

@ -3,6 +3,7 @@ package handlers
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
@ -50,9 +51,10 @@ func VerifyEmailHandler() gin.HandlerFunc {
db.Mgr.DeleteToken(claim.Email) db.Mgr.DeleteToken(claim.Email)
userIdStr := fmt.Sprintf("%v", user.ID) userIdStr := fmt.Sprintf("%v", user.ID)
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, user.Roles) roles := strings.Split(user.Roles, ",")
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, roles)
accessToken, _, _ := utils.CreateAuthToken(user, enum.AccessToken, user.Roles) accessToken, _, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
session.SetToken(userIdStr, refreshToken) session.SetToken(userIdStr, refreshToken)
utils.SetCookie(c, accessToken) utils.SetCookie(c, accessToken)

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum" "github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
@ -91,7 +92,7 @@ func AdminUpdateUser(ctx context.Context, params model.AdminUpdateUserInput) (*m
inputRoles = append(inputRoles, *item) inputRoles = append(inputRoles, *item)
} }
if !utils.IsValidRolesArray(inputRoles) { if !utils.IsValidRoles(append([]string{}, append(constants.ROLES, constants.PROTECTED_ROLES...)...), inputRoles) {
return res, fmt.Errorf("invalid list of roles") return res, fmt.Errorf("invalid list of roles")
} }

View File

@ -46,19 +46,19 @@ func Login(ctx context.Context, params model.LoginInput) (*model.AuthResponse, e
log.Println("Compare password error:", err) log.Println("Compare password error:", err)
return res, fmt.Errorf(`invalid password`) return res, fmt.Errorf(`invalid password`)
} }
role := constants.DEFAULT_ROLE roles := constants.DEFAULT_ROLES
if params.Role != nil { currentRoles := strings.Split(user.Roles, ",")
// validate role if len(params.Roles) > 0 {
if !utils.IsValidRole(strings.Split(user.Roles, ","), *params.Role) { if !utils.IsValidRoles(currentRoles, params.Roles) {
return res, fmt.Errorf(`invalid role`) return res, fmt.Errorf(`invalid roles`)
} }
role = *params.Role roles = params.Roles
} }
userIdStr := fmt.Sprintf("%v", user.ID) userIdStr := fmt.Sprintf("%v", user.ID)
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, role) refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, roles)
accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, role) accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
session.SetToken(userIdStr, refreshToken) session.SetToken(userIdStr, refreshToken)

View File

@ -37,16 +37,15 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
inputRoles := []string{} inputRoles := []string{}
if params.Roles != nil && len(params.Roles) > 0 { if len(params.Roles) > 0 {
// check if roles exists // check if roles exists
for _, item := range params.Roles { if !utils.IsValidRoles(constants.ROLES, params.Roles) {
inputRoles = append(inputRoles, *item)
}
if !utils.IsValidRolesArray(inputRoles) {
return res, fmt.Errorf(`invalid roles`) return res, fmt.Errorf(`invalid roles`)
} else {
inputRoles = params.Roles
} }
} else { } else {
inputRoles = []string{constants.DEFAULT_ROLE} inputRoles = constants.DEFAULT_ROLES
} }
// find user with email // find user with email
@ -85,6 +84,7 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
return res, err return res, err
} }
userIdStr := fmt.Sprintf("%v", user.ID) userIdStr := fmt.Sprintf("%v", user.ID)
roles := strings.Split(user.Roles, ",")
userToReturn := &model.User{ userToReturn := &model.User{
ID: userIdStr, ID: userIdStr,
Email: user.Email, Email: user.Email,
@ -123,9 +123,9 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse,
} }
} else { } else {
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, constants.DEFAULT_ROLE) refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, roles)
accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, constants.DEFAULT_ROLE) accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
session.SetToken(userIdStr, refreshToken) session.SetToken(userIdStr, refreshToken)
res = &model.AuthResponse{ res = &model.AuthResponse{

View File

@ -14,7 +14,7 @@ import (
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
) )
func Token(ctx context.Context, role *string) (*model.AuthResponse, error) { func Token(ctx context.Context, roles []string) (*model.AuthResponse, error) {
var res *model.AuthResponse var res *model.AuthResponse
gc, err := utils.GinContextFromContext(ctx) gc, err := utils.GinContextFromContext(ctx)
@ -30,16 +30,11 @@ func Token(ctx context.Context, role *string) (*model.AuthResponse, error) {
expiresAt := claim["exp"].(int64) expiresAt := claim["exp"].(int64)
email := fmt.Sprintf("%v", claim["email"]) email := fmt.Sprintf("%v", claim["email"])
claimRole := fmt.Sprintf("%v", claim[constants.JWT_ROLE_CLAIM])
user, err := db.Mgr.GetUserByEmail(email) user, err := db.Mgr.GetUserByEmail(email)
if err != nil { if err != nil {
return res, err return res, err
} }
if role != nil && *role != claimRole {
return res, fmt.Errorf(`unauthorized`)
}
userIdStr := fmt.Sprintf("%v", user.ID) userIdStr := fmt.Sprintf("%v", user.ID)
sessionToken := session.GetToken(userIdStr) sessionToken := session.GetToken(userIdStr)
@ -47,15 +42,30 @@ func Token(ctx context.Context, role *string) (*model.AuthResponse, error) {
if sessionToken == "" { if sessionToken == "" {
return res, fmt.Errorf(`unauthorized`) return res, fmt.Errorf(`unauthorized`)
} }
// TODO check if refresh/session token has expired
expiresTimeObj := time.Unix(expiresAt, 0) expiresTimeObj := time.Unix(expiresAt, 0)
currentTimeObj := time.Now() currentTimeObj := time.Now()
claimRoleInterface := claim[constants.JWT_ROLE_CLAIM].([]interface{})
claimRoles := make([]string, len(claimRoleInterface))
for i, v := range claimRoleInterface {
claimRoles[i] = v.(string)
}
if len(roles) > 0 {
for _, v := range roles {
if !utils.StringContains(claimRoles, v) {
return res, fmt.Errorf(`unauthorized`)
}
}
}
if accessTokenErr != nil || expiresTimeObj.Sub(currentTimeObj).Minutes() <= 5 { if accessTokenErr != nil || expiresTimeObj.Sub(currentTimeObj).Minutes() <= 5 {
// if access token has expired and refresh/session token is valid // if access token has expired and refresh/session token is valid
// generate new accessToken // generate new accessToken
token, expiresAt, _ = utils.CreateAuthToken(user, enum.AccessToken, claimRole) token, expiresAt, _ = utils.CreateAuthToken(user, enum.AccessToken, claimRoles)
} }
utils.SetCookie(gc, token) utils.SetCookie(gc, token)
res = &model.AuthResponse{ res = &model.AuthResponse{
Message: `Token verified`, Message: `Token verified`,

View File

@ -124,31 +124,6 @@ func UpdateProfile(ctx context.Context, params model.UpdateProfileInput) (*model
}() }()
} }
// TODO this idea needs to be verified otherwise every user can make themselves super admin
// rolesToSave := ""
// if params.Roles != nil && len(params.Roles) > 0 {
// currentRoles := strings.Split(user.Roles, ",")
// inputRoles := []string{}
// for _, item := range params.Roles {
// inputRoles = append(inputRoles, *item)
// }
// if !utils.IsValidRolesArray(inputRoles) {
// return res, fmt.Errorf("invalid list of roles")
// }
// if !utils.IsStringArrayEqual(inputRoles, currentRoles) {
// rolesToSave = strings.Join(inputRoles, ",")
// }
// session.DeleteToken(fmt.Sprintf("%v", user.ID))
// utils.DeleteCookie(gc)
// }
// if rolesToSave != "" {
// user.Roles = rolesToSave
// }
_, err = db.Mgr.UpdateUser(user) _, err = db.Mgr.UpdateUser(user)
if err != nil { if err != nil {
log.Println("Error updating user:", err) log.Println("Error updating user:", err)

View File

@ -6,7 +6,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/enum" "github.com/authorizerdev/authorizer/server/enum"
"github.com/authorizerdev/authorizer/server/graph/model" "github.com/authorizerdev/authorizer/server/graph/model"
@ -43,9 +42,10 @@ func VerifyEmail(ctx context.Context, params model.VerifyEmailInput) (*model.Aut
db.Mgr.DeleteToken(claim.Email) db.Mgr.DeleteToken(claim.Email)
userIdStr := fmt.Sprintf("%v", user.ID) userIdStr := fmt.Sprintf("%v", user.ID)
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, constants.DEFAULT_ROLE) roles := strings.Split(user.Roles, ",")
refreshToken, _, _ := utils.CreateAuthToken(user, enum.RefreshToken, roles)
accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, constants.DEFAULT_ROLE) accessToken, expiresAt, _ := utils.CreateAuthToken(user, enum.AccessToken, roles)
session.SetToken(userIdStr, refreshToken) session.SetToken(userIdStr, refreshToken)

View File

@ -26,7 +26,7 @@ type UserAuthClaim struct {
*JWTCustomClaim `json:"authorizer"` *JWTCustomClaim `json:"authorizer"`
} }
func CreateAuthToken(user db.User, tokenType enum.TokenType, role string) (string, int64, error) { func CreateAuthToken(user db.User, tokenType enum.TokenType, roles []string) (string, int64, error) {
t := jwt.New(jwt.GetSigningMethod(constants.JWT_TYPE)) t := jwt.New(jwt.GetSigningMethod(constants.JWT_TYPE))
expiryBound := time.Hour expiryBound := time.Hour
if tokenType == enum.RefreshToken { if tokenType == enum.RefreshToken {
@ -41,7 +41,7 @@ func CreateAuthToken(user db.User, tokenType enum.TokenType, role string) (strin
"email": user.Email, "email": user.Email,
"id": user.ID, "id": user.ID,
"allowed_roles": strings.Split(user.Roles, ","), "allowed_roles": strings.Split(user.Roles, ","),
constants.JWT_ROLE_CLAIM: role, constants.JWT_ROLE_CLAIM: roles,
} }
t.Claims = &UserAuthClaim{ t.Claims = &UserAuthClaim{

View File

@ -18,3 +18,12 @@ func WriteToFile(filename string, data string) error {
} }
return file.Sync() return file.Sync()
} }
func StringContains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}

View File

@ -40,30 +40,14 @@ func IsSuperAdmin(gc *gin.Context) bool {
return secret == constants.ADMIN_SECRET return secret == constants.ADMIN_SECRET
} }
func IsValidRolesArray(roles []string) bool { func IsValidRoles(userRoles []string, roles []string) bool {
valid := true valid := true
currentRoleMap := map[string]bool{} for _, role := range roles {
if !StringContains(userRoles, role) {
for _, currentRole := range constants.ROLES {
currentRoleMap[currentRole] = true
}
for _, inputRole := range roles {
if !currentRoleMap[inputRole] {
valid = false valid = false
break break
} }
} }
return valid
}
func IsValidRole(userRoles []string, role string) bool {
valid := false
for _, currentRole := range userRoles {
if role == currentRole {
valid = true
break
}
}
return valid return valid
} }