Compare commits

...

16 Commits

Author SHA1 Message Date
Lakhan Samani
d1e284116d fix: verification request model 2022-03-09 07:10:07 +05:30
Lakhan Samani
2f9725d8e1 fix: verification request 2022-03-09 06:41:38 +05:30
Lakhan Samani
ee7aea7bee fix: verify email 2022-03-08 22:55:45 +05:30
Lakhan Samani
5d73df0040 fix: magic link login 2022-03-08 22:41:33 +05:30
Lakhan Samani
60cd317e67 fix: add redirect url to logout 2022-03-08 21:32:42 +05:30
Lakhan Samani
f5bdc8db39 fix: refresh token store info 2022-03-08 21:13:23 +05:30
Lakhan Samani
9eca697a91 fix: refresh token param in string 2022-03-08 19:31:19 +05:30
Lakhan Samani
7136ee924d fix: rotate refresh token 2022-03-08 19:18:33 +05:30
Lakhan Samani
fd9eb7c733 fix: oauth state split 2022-03-08 19:13:45 +05:30
Lakhan Samani
917eaeb2ed feat: don't set cookie in case of offline_access 2022-03-08 18:51:46 +05:30
Lakhan Samani
3bb90acc9e feat: add revoke mutation + handler 2022-03-08 18:49:42 +05:30
Lakhan Samani
a69b8e290c feat: add ability to get access token based on refresh token 2022-03-08 14:56:46 +05:30
Lakhan Samani
674eeeea4e chore: bump authorizer-react 2022-03-08 14:20:11 +05:30
Lakhan Samani
8c2bf6ee0d fix: add token information in redirect url 2022-03-08 12:36:26 +05:30
Lakhan Samani
57bc091499 fix state management 2022-03-07 23:44:19 +05:30
Lakhan Samani
128a2a8f75 feat: add support for response mode 2022-03-07 18:49:18 +05:30
39 changed files with 972 additions and 401 deletions

30
app/package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-react": "0.7.0", "@authorizerdev/authorizer-react": "0.9.0-beta.3",
"@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",
@@ -24,9 +24,9 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-js": { "node_modules/@authorizerdev/authorizer-js": {
"version": "0.3.0", "version": "0.4.0-beta.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.3.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.0.tgz",
"integrity": "sha512-KCE5Dw5MUnEgstBUayBriDQAOjqbxU7ixC00rTHAE6aD6TxJkeSls0vCTXpvt4iiKhFK6q9BhHwa/5NwWYpDBQ==", "integrity": "sha512-wNh5ROldNqdbOXFPDlq1tObzPZyEQkbnOvSEwvnDfPYb9/BsJ3naj3/ayz4J2R5k2+Eyuk0LK64XYdkfIW0HYA==",
"dependencies": { "dependencies": {
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
}, },
@@ -35,11 +35,11 @@
} }
}, },
"node_modules/@authorizerdev/authorizer-react": { "node_modules/@authorizerdev/authorizer-react": {
"version": "0.7.0", "version": "0.9.0-beta.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.7.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.3.tgz",
"integrity": "sha512-cAxUhodftIveSQt+rFuEA0CxjmbpVfE43ioZBwBxqWEuJHPdPH7bohOQRgTyA2xb3QVnh7kr607Tau13DO7qUA==", "integrity": "sha512-P93PW6W3Qm9BW3160gn0Ce+64UCFAOpoEOHf5537LgFPE8LpNAIU3EI6EtMNkOJS58pu1h2UkfyRyX/j0Pohjw==",
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": "^0.3.0", "@authorizerdev/authorizer-js": "^0.4.0-beta.0",
"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"
@@ -829,19 +829,19 @@
}, },
"dependencies": { "dependencies": {
"@authorizerdev/authorizer-js": { "@authorizerdev/authorizer-js": {
"version": "0.3.0", "version": "0.4.0-beta.0",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.3.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.0.tgz",
"integrity": "sha512-KCE5Dw5MUnEgstBUayBriDQAOjqbxU7ixC00rTHAE6aD6TxJkeSls0vCTXpvt4iiKhFK6q9BhHwa/5NwWYpDBQ==", "integrity": "sha512-wNh5ROldNqdbOXFPDlq1tObzPZyEQkbnOvSEwvnDfPYb9/BsJ3naj3/ayz4J2R5k2+Eyuk0LK64XYdkfIW0HYA==",
"requires": { "requires": {
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
} }
}, },
"@authorizerdev/authorizer-react": { "@authorizerdev/authorizer-react": {
"version": "0.7.0", "version": "0.9.0-beta.3",
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.7.0.tgz", "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.3.tgz",
"integrity": "sha512-cAxUhodftIveSQt+rFuEA0CxjmbpVfE43ioZBwBxqWEuJHPdPH7bohOQRgTyA2xb3QVnh7kr607Tau13DO7qUA==", "integrity": "sha512-P93PW6W3Qm9BW3160gn0Ce+64UCFAOpoEOHf5537LgFPE8LpNAIU3EI6EtMNkOJS58pu1h2UkfyRyX/j0Pohjw==",
"requires": { "requires": {
"@authorizerdev/authorizer-js": "^0.3.0", "@authorizerdev/authorizer-js": "^0.4.0-beta.0",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.3",
"styled-components": "^5.3.0" "styled-components": "^5.3.0"

View File

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

@@ -30,15 +30,7 @@ export default function App() {
/> />
<h1>{globalState.organizationName}</h1> <h1>{globalState.organizationName}</h1>
</div> </div>
<div <div className="container">
style={{
width: 400,
margin: `10px auto`,
border: `1px solid #D1D5DB`,
padding: `25px 20px`,
borderRadius: 5,
}}
>
<BrowserRouter> <BrowserRouter>
<AuthorizerProvider <AuthorizerProvider
config={{ config={{

View File

@@ -11,9 +11,19 @@ export default function Root() {
useEffect(() => { useEffect(() => {
if (token) { if (token) {
const url = new URL(config.redirectURL || '/app'); console.log({ token });
let redirectURL = config.redirectURL || '/app';
const params = `access_token=${token.access_token}&id_token=${token.id_token}&expires_in=${token.expires_in}&refresh_token=${token.refresh_token}`;
const url = new URL(redirectURL);
if (redirectURL.includes('?')) {
redirectURL = `${redirectURL}&${params}`;
} else {
redirectURL = `${redirectURL}?${params}`;
}
if (url.origin !== window.location.origin) { if (url.origin !== window.location.origin) {
window.location.href = config.redirectURL || '/app'; sessionStorage.removeItem('authorizer_state');
window.location.replace(redirectURL);
} }
} }
return () => {}; return () => {};

View File

@@ -1,5 +1,5 @@
body { body {
margin: 0; margin: 10;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif; sans-serif;
@@ -14,3 +14,17 @@ body {
*:after { *:after {
box-sizing: inherit; box-sizing: inherit;
} }
.container {
box-sizing: content-box;
border: 1px solid #d1d5db;
padding: 25px 20px;
border-radius: 5px;
}
@media only screen and (min-width: 768px) {
.container {
width: 400px;
margin: 0 auto;
}
}

View File

@@ -94,6 +94,7 @@ func EncryptEnvData(data envstore.Store) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
encryptedConfig, err := EncryptAESEnv(configData) encryptedConfig, err := EncryptAESEnv(configData)
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -4,25 +4,28 @@ import "github.com/authorizerdev/authorizer/server/graph/model"
// VerificationRequest model for db // VerificationRequest model for db
type VerificationRequest struct { type VerificationRequest struct {
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"` ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
Token string `gorm:"type:text" json:"token" bson:"token"` Token string `gorm:"type:text" json:"token" bson:"token"`
Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"` Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"`
ExpiresAt int64 `json:"expires_at" bson:"expires_at"` ExpiresAt int64 `json:"expires_at" bson:"expires_at"`
CreatedAt int64 `json:"created_at" bson:"created_at"` CreatedAt int64 `json:"created_at" bson:"created_at"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"` Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
Nonce string `gorm:"type:char(36)" json:"nonce" bson:"nonce"` Nonce string `gorm:"type:text" json:"nonce" bson:"nonce"`
RedirectURI string `gorm:"type:text" json:"redirect_uri" bson:"redirect_uri"`
} }
func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest { func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest {
return &model.VerificationRequest{ return &model.VerificationRequest{
ID: v.ID, ID: v.ID,
Token: &v.Token, Token: &v.Token,
Identifier: &v.Identifier, Identifier: &v.Identifier,
Expires: &v.ExpiresAt, Expires: &v.ExpiresAt,
CreatedAt: &v.CreatedAt, CreatedAt: &v.CreatedAt,
UpdatedAt: &v.UpdatedAt, UpdatedAt: &v.UpdatedAt,
Email: &v.Email, Email: &v.Email,
Nonce: &v.Nonce,
RedirectURI: &v.RedirectURI,
} }
} }

View File

@@ -21,7 +21,7 @@ func (p *provider) AddVerificationRequest(verificationRequest models.Verificatio
verificationRequest.UpdatedAt = time.Now().Unix() verificationRequest.UpdatedAt = time.Now().Unix()
result := p.db.Clauses(clause.OnConflict{ result := p.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}}, Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}},
DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at"}), DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at", "nonce", "redirect_uri"}),
}).Create(&verificationRequest) }).Create(&verificationRequest)
if result.Error != nil { if result.Error != nil {

View File

@@ -1,6 +1,8 @@
package email package email
import ( import (
"fmt"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
) )
@@ -103,5 +105,9 @@ func SendVerificationMail(toEmail, token, hostname string) error {
message = addEmailTemplate(message, data, "verify_email.tmpl") message = addEmailTemplate(message, data, "verify_email.tmpl")
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message) // bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
return SendMail(Receiver, Subject, message) err := SendMail(Receiver, Subject, message)
if err != nil {
fmt.Println("=> error sending email:", err)
}
return err
} }

View File

@@ -112,7 +112,7 @@ func PersistEnv() error {
for key, value := range storeData.StringEnv { for key, value := range storeData.StringEnv {
// don't override unexposed envs // don't override unexposed envs
if key != constants.EnvKeyEncryptionKey && key != constants.EnvKeyClientID && key != constants.EnvKeyClientSecret && key != constants.EnvKeyJWK { if key != constants.EnvKeyEncryptionKey {
// check only for derivative keys // check only for derivative keys
// No need to check for ENCRYPTION_KEY which special key we use for encrypting config data // No need to check for ENCRYPTION_KEY which special key we use for encrypting config data
// as we have removed it from json // as we have removed it from json

View File

@@ -119,6 +119,7 @@ type ComplexityRoot struct {
MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int
ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int
ResetPassword func(childComplexity int, params model.ResetPasswordInput) int ResetPassword func(childComplexity int, params model.ResetPasswordInput) int
Revoke func(childComplexity int, params model.OAuthRevokeInput) int
Signup func(childComplexity int, params model.SignUpInput) int Signup func(childComplexity int, params model.SignUpInput) int
UpdateEnv func(childComplexity int, params model.UpdateEnvInput) int UpdateEnv func(childComplexity int, params model.UpdateEnvInput) int
UpdateProfile func(childComplexity int, params model.UpdateProfileInput) int UpdateProfile func(childComplexity int, params model.UpdateProfileInput) int
@@ -173,13 +174,15 @@ type ComplexityRoot struct {
} }
VerificationRequest struct { VerificationRequest struct {
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
Email func(childComplexity int) int Email func(childComplexity int) int
Expires func(childComplexity int) int Expires func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
Identifier func(childComplexity int) int Identifier func(childComplexity int) int
Token func(childComplexity int) int Nonce func(childComplexity int) int
UpdatedAt func(childComplexity int) int RedirectURI func(childComplexity int) int
Token func(childComplexity int) int
UpdatedAt func(childComplexity int) int
} }
VerificationRequests struct { VerificationRequests struct {
@@ -198,6 +201,7 @@ type MutationResolver interface {
ResendVerifyEmail(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, error) ResendVerifyEmail(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, error)
ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error) ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error)
ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error) ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error)
Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error)
DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error)
UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error) UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error)
AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error) AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error)
@@ -711,6 +715,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.ResetPassword(childComplexity, args["params"].(model.ResetPasswordInput)), true return e.complexity.Mutation.ResetPassword(childComplexity, args["params"].(model.ResetPasswordInput)), true
case "Mutation.revoke":
if e.complexity.Mutation.Revoke == nil {
break
}
args, err := ec.field_Mutation_revoke_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.Revoke(childComplexity, args["params"].(model.OAuthRevokeInput)), true
case "Mutation.signup": case "Mutation.signup":
if e.complexity.Mutation.Signup == nil { if e.complexity.Mutation.Signup == nil {
break break
@@ -1038,6 +1054,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.VerificationRequest.Identifier(childComplexity), true return e.complexity.VerificationRequest.Identifier(childComplexity), true
case "VerificationRequest.nonce":
if e.complexity.VerificationRequest.Nonce == nil {
break
}
return e.complexity.VerificationRequest.Nonce(childComplexity), true
case "VerificationRequest.redirect_uri":
if e.complexity.VerificationRequest.RedirectURI == nil {
break
}
return e.complexity.VerificationRequest.RedirectURI(childComplexity), true
case "VerificationRequest.token": case "VerificationRequest.token":
if e.complexity.VerificationRequest.Token == nil { if e.complexity.VerificationRequest.Token == nil {
break break
@@ -1189,6 +1219,8 @@ type VerificationRequest {
expires: Int64 expires: Int64
created_at: Int64 created_at: Int64
updated_at: Int64 updated_at: Int64
nonce: String
redirect_uri: String
} }
type VerificationRequests { type VerificationRequests {
@@ -1361,6 +1393,8 @@ input UpdateUserInput {
input ForgotPasswordInput { input ForgotPasswordInput {
email: String! email: String!
state: String
redirect_uri: String
} }
input ResetPasswordInput { input ResetPasswordInput {
@@ -1377,6 +1411,8 @@ input MagicLinkLoginInput {
email: String! email: String!
roles: [String!] roles: [String!]
scope: [String!] scope: [String!]
state: String
redirect_uri: String
} }
input SessionQueryInput { input SessionQueryInput {
@@ -1393,6 +1429,10 @@ input PaginatedInput {
pagination: PaginationInput pagination: PaginationInput
} }
input OAuthRevokeInput {
refresh_token: String!
}
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
@@ -1403,6 +1443,7 @@ type Mutation {
resend_verify_email(params: ResendVerifyEmailInput!): Response! resend_verify_email(params: ResendVerifyEmailInput!): Response!
forgot_password(params: ForgotPasswordInput!): Response! forgot_password(params: ForgotPasswordInput!): Response!
reset_password(params: ResetPasswordInput!): Response! reset_password(params: ResetPasswordInput!): Response!
revoke(params: OAuthRevokeInput!): Response!
# admin only apis # admin only apis
_delete_user(params: DeleteUserInput!): Response! _delete_user(params: DeleteUserInput!): Response!
_update_user(params: UpdateUserInput!): User! _update_user(params: UpdateUserInput!): User!
@@ -1580,6 +1621,21 @@ func (ec *executionContext) field_Mutation_reset_password_args(ctx context.Conte
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation_revoke_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 model.OAuthRevokeInput
if tmp, ok := rawArgs["params"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params"))
arg0, err = ec.unmarshalNOAuthRevokeInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐOAuthRevokeInput(ctx, tmp)
if err != nil {
return nil, err
}
}
args["params"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_signup_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Mutation_signup_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{}{}
@@ -3838,6 +3894,48 @@ func (ec *executionContext) _Mutation_reset_password(ctx context.Context, field
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res) return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
} }
func (ec *executionContext) _Mutation_revoke(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_revoke_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().Revoke(rctx, args["params"].(model.OAuthRevokeInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*model.Response)
fc.Result = res
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -5451,6 +5549,70 @@ func (ec *executionContext) _VerificationRequest_updated_at(ctx context.Context,
return ec.marshalOInt642ᚖint64(ctx, field.Selections, res) return ec.marshalOInt642ᚖint64(ctx, field.Selections, res)
} }
func (ec *executionContext) _VerificationRequest_nonce(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequest) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VerificationRequest",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Nonce, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _VerificationRequest_redirect_uri(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequest) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "VerificationRequest",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.RedirectURI, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _VerificationRequests_pagination(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequests) (ret graphql.Marshaler) { func (ec *executionContext) _VerificationRequests_pagination(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequests) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -6729,6 +6891,22 @@ func (ec *executionContext) unmarshalInputForgotPasswordInput(ctx context.Contex
if err != nil { if err != nil {
return it, err return it, err
} }
case "state":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
case "redirect_uri":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri"))
it.RedirectURI, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
} }
} }
@@ -6815,6 +6993,45 @@ func (ec *executionContext) unmarshalInputMagicLinkLoginInput(ctx context.Contex
if err != nil { if err != nil {
return it, err return it, err
} }
case "state":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
case "redirect_uri":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri"))
it.RedirectURI, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputOAuthRevokeInput(ctx context.Context, obj interface{}) (model.OAuthRevokeInput, error) {
var it model.OAuthRevokeInput
asMap := map[string]interface{}{}
for k, v := range obj.(map[string]interface{}) {
asMap[k] = v
}
for k, v := range asMap {
switch k {
case "refresh_token":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("refresh_token"))
it.RefreshToken, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
} }
} }
@@ -7921,6 +8138,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "revoke":
out.Values[i] = ec._Mutation_revoke(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "_delete_user": case "_delete_user":
out.Values[i] = ec._Mutation__delete_user(ctx, field) out.Values[i] = ec._Mutation__delete_user(ctx, field)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@@ -8290,6 +8512,10 @@ func (ec *executionContext) _VerificationRequest(ctx context.Context, sel ast.Se
out.Values[i] = ec._VerificationRequest_created_at(ctx, field, obj) out.Values[i] = ec._VerificationRequest_created_at(ctx, field, obj)
case "updated_at": case "updated_at":
out.Values[i] = ec._VerificationRequest_updated_at(ctx, field, obj) out.Values[i] = ec._VerificationRequest_updated_at(ctx, field, obj)
case "nonce":
out.Values[i] = ec._VerificationRequest_nonce(ctx, field, obj)
case "redirect_uri":
out.Values[i] = ec._VerificationRequest_redirect_uri(ctx, field, obj)
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@@ -8700,6 +8926,11 @@ func (ec *executionContext) marshalNMeta2ᚖgithubᚗcomᚋauthorizerdevᚋautho
return ec._Meta(ctx, sel, v) return ec._Meta(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNOAuthRevokeInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐOAuthRevokeInput(ctx context.Context, v interface{}) (model.OAuthRevokeInput, error) {
res, err := ec.unmarshalInputOAuthRevokeInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNPagination2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐPagination(ctx context.Context, sel ast.SelectionSet, v *model.Pagination) graphql.Marshaler { func (ec *executionContext) marshalNPagination2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐPagination(ctx context.Context, sel ast.SelectionSet, v *model.Pagination) graphql.Marshaler {
if v == nil { if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {

View File

@@ -69,7 +69,9 @@ type Error struct {
} }
type ForgotPasswordInput struct { type ForgotPasswordInput struct {
Email string `json:"email"` Email string `json:"email"`
State *string `json:"state"`
RedirectURI *string `json:"redirect_uri"`
} }
type LoginInput struct { type LoginInput struct {
@@ -80,9 +82,11 @@ type LoginInput struct {
} }
type MagicLinkLoginInput struct { type MagicLinkLoginInput struct {
Email string `json:"email"` Email string `json:"email"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
Scope []string `json:"scope"` Scope []string `json:"scope"`
State *string `json:"state"`
RedirectURI *string `json:"redirect_uri"`
} }
type Meta struct { type Meta struct {
@@ -96,6 +100,10 @@ type Meta struct {
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
} }
type OAuthRevokeInput struct {
RefreshToken string `json:"refresh_token"`
}
type PaginatedInput struct { type PaginatedInput struct {
Pagination *PaginationInput `json:"pagination"` Pagination *PaginationInput `json:"pagination"`
} }
@@ -239,13 +247,15 @@ type Users struct {
} }
type VerificationRequest struct { type VerificationRequest struct {
ID string `json:"id"` ID string `json:"id"`
Identifier *string `json:"identifier"` Identifier *string `json:"identifier"`
Token *string `json:"token"` Token *string `json:"token"`
Email *string `json:"email"` Email *string `json:"email"`
Expires *int64 `json:"expires"` Expires *int64 `json:"expires"`
CreatedAt *int64 `json:"created_at"` CreatedAt *int64 `json:"created_at"`
UpdatedAt *int64 `json:"updated_at"` UpdatedAt *int64 `json:"updated_at"`
Nonce *string `json:"nonce"`
RedirectURI *string `json:"redirect_uri"`
} }
type VerificationRequests struct { type VerificationRequests struct {

View File

@@ -57,6 +57,8 @@ type VerificationRequest {
expires: Int64 expires: Int64
created_at: Int64 created_at: Int64
updated_at: Int64 updated_at: Int64
nonce: String
redirect_uri: String
} }
type VerificationRequests { type VerificationRequests {
@@ -229,6 +231,8 @@ input UpdateUserInput {
input ForgotPasswordInput { input ForgotPasswordInput {
email: String! email: String!
state: String
redirect_uri: String
} }
input ResetPasswordInput { input ResetPasswordInput {
@@ -245,6 +249,8 @@ input MagicLinkLoginInput {
email: String! email: String!
roles: [String!] roles: [String!]
scope: [String!] scope: [String!]
state: String
redirect_uri: String
} }
input SessionQueryInput { input SessionQueryInput {
@@ -261,6 +267,10 @@ input PaginatedInput {
pagination: PaginationInput pagination: PaginationInput
} }
input OAuthRevokeInput {
refresh_token: String!
}
type Mutation { type Mutation {
signup(params: SignUpInput!): AuthResponse! signup(params: SignUpInput!): AuthResponse!
login(params: LoginInput!): AuthResponse! login(params: LoginInput!): AuthResponse!
@@ -271,6 +281,7 @@ type Mutation {
resend_verify_email(params: ResendVerifyEmailInput!): Response! resend_verify_email(params: ResendVerifyEmailInput!): Response!
forgot_password(params: ForgotPasswordInput!): Response! forgot_password(params: ForgotPasswordInput!): Response!
reset_password(params: ResetPasswordInput!): Response! reset_password(params: ResetPasswordInput!): Response!
revoke(params: OAuthRevokeInput!): Response!
# admin only apis # admin only apis
_delete_user(params: DeleteUserInput!): Response! _delete_user(params: DeleteUserInput!): Response!
_update_user(params: UpdateUserInput!): User! _update_user(params: UpdateUserInput!): User!

View File

@@ -47,6 +47,10 @@ func (r *mutationResolver) ResetPassword(ctx context.Context, params model.Reset
return resolvers.ResetPasswordResolver(ctx, params) return resolvers.ResetPasswordResolver(ctx, params)
} }
func (r *mutationResolver) Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) {
return resolvers.RevokeResolver(ctx, params)
}
func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) { func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) {
return resolvers.DeleteUserResolver(ctx, params) return resolvers.DeleteUserResolver(ctx, params)
} }
@@ -105,5 +109,7 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol
// Query returns generated.QueryResolver implementation. // Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver } type (
type queryResolver struct{ *Resolver } mutationResolver struct{ *Resolver }
queryResolver struct{ *Resolver }
)

View File

@@ -1,13 +1,11 @@
package handlers package handlers
import ( import (
"encoding/json"
"log" "log"
"net/http" "net/http"
"strings" "strings"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/crypto"
"github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -29,44 +27,25 @@ func AppHandler() gin.HandlerFunc {
return return
} }
state := c.Query("state") redirect_uri := strings.TrimSpace(c.Query("redirect_uri"))
state := strings.TrimSpace(c.Query("state"))
scopeString := strings.TrimSpace(c.Query("scope"))
var stateObj State var scope []string
if scopeString == "" {
if state == "" { scope = []string{"openid", "profile", "email"}
stateObj.AuthorizerURL = hostname
stateObj.RedirectURL = hostname + "/app"
} else { } else {
decodedState, err := crypto.DecryptB64(state) scope = strings.Split(scopeString, " ")
if err != nil { }
c.JSON(400, gin.H{"error": "[unable to decode state] invalid state"})
return
}
err = json.Unmarshal([]byte(decodedState), &stateObj)
if err != nil {
c.JSON(400, gin.H{"error": "[unable to parse state] invalid state"})
return
}
stateObj.AuthorizerURL = strings.TrimSuffix(stateObj.AuthorizerURL, "/")
stateObj.RedirectURL = strings.TrimSuffix(stateObj.RedirectURL, "/")
if redirect_uri == "" {
redirect_uri = hostname + "/app"
} else {
// validate redirect url with allowed origins // validate redirect url with allowed origins
if !utils.IsValidOrigin(stateObj.RedirectURL) { if !utils.IsValidOrigin(redirect_uri) {
c.JSON(400, gin.H{"error": "invalid redirect url"}) c.JSON(400, gin.H{"error": "invalid redirect url"})
return return
} }
if stateObj.AuthorizerURL == "" {
c.JSON(400, gin.H{"error": "invalid authorizer url"})
return
}
// validate host and domain of authorizer url
if strings.TrimSuffix(stateObj.AuthorizerURL, "/") != hostname {
c.JSON(400, gin.H{"error": "invalid host url"})
return
}
} }
// debug the request state // debug the request state
@@ -77,9 +56,11 @@ func AppHandler() gin.HandlerFunc {
} }
} }
c.HTML(http.StatusOK, "app.tmpl", gin.H{ c.HTML(http.StatusOK, "app.tmpl", gin.H{
"data": map[string]string{ "data": map[string]interface{}{
"authorizerURL": stateObj.AuthorizerURL, "authorizerURL": hostname,
"redirectURL": stateObj.RedirectURL, "redirectURL": redirect_uri,
"scope": scope,
"state": state,
"organizationName": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName), "organizationName": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName),
"organizationLogo": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo), "organizationLogo": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo),
}, },

View File

@@ -2,6 +2,7 @@ package handlers
import ( import (
"net/http" "net/http"
"strconv"
"strings" "strings"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
@@ -17,6 +18,7 @@ import (
// AuthorizeHandler is the handler for the /authorize route // AuthorizeHandler is the handler for the /authorize route
// required params // required params
// ?redirect_uri = redirect url // ?redirect_uri = redirect url
// ?response_mode = to decide if result should be html or re-direct
// state[recommended] = to prevent CSRF attack (for authorizer its compulsory) // state[recommended] = to prevent CSRF attack (for authorizer its compulsory)
// code_challenge = to prevent CSRF attack // code_challenge = to prevent CSRF attack
// code_challenge_method = to prevent CSRF attack [only sh256 is supported] // code_challenge_method = to prevent CSRF attack [only sh256 is supported]
@@ -31,62 +33,7 @@ func AuthorizeHandler() gin.HandlerFunc {
scopeString := strings.TrimSpace(gc.Query("scope")) scopeString := strings.TrimSpace(gc.Query("scope"))
clientID := strings.TrimSpace(gc.Query("client_id")) clientID := strings.TrimSpace(gc.Query("client_id"))
template := "authorize.tmpl" template := "authorize.tmpl"
responseMode := strings.TrimSpace(gc.Query("response_mode"))
if clientID == "" {
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "client_id is required",
},
},
})
return
}
if clientID != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) {
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "invalid_client_id",
},
},
})
return
}
if redirectURI == "" {
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "redirect_uri is required",
},
},
})
return
}
if state == "" {
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "state is required",
},
},
})
return
}
if responseType == "" {
responseType = "token"
}
var scope []string var scope []string
if scopeString == "" { if scopeString == "" {
@@ -95,80 +42,171 @@ func AuthorizeHandler() gin.HandlerFunc {
scope = strings.Split(scopeString, " ") scope = strings.Split(scopeString, " ")
} }
if responseMode == "" {
responseMode = "query"
}
if responseMode != "query" && responseMode != "web_message" {
gc.JSON(400, gin.H{"error": "invalid response mode"})
}
if redirectURI == "" {
redirectURI = "/app"
}
isQuery := responseMode == "query"
loginURL := "/app?state=" + state + "&scope=" + strings.Join(scope, " ") + "&redirect_uri=" + redirectURI
if clientID == "" {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "client_id is required",
},
},
})
}
return
}
if clientID != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "invalid_client_id",
},
},
})
}
return
}
if state == "" {
if isQuery {
gc.Redirect(http.StatusFound, loginURL)
} else {
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "state is required",
},
},
})
}
return
}
if responseType == "" {
responseType = "token"
}
isResponseTypeCode := responseType == "code" isResponseTypeCode := responseType == "code"
isResponseTypeToken := responseType == "token" isResponseTypeToken := responseType == "token"
if !isResponseTypeCode && !isResponseTypeToken { if !isResponseTypeCode && !isResponseTypeToken {
gc.HTML(http.StatusOK, template, gin.H{ if isQuery {
"target_origin": redirectURI, gc.Redirect(http.StatusFound, loginURL)
"authorization_response": map[string]interface{}{ } else {
"type": "authorization_response", gc.HTML(http.StatusOK, template, gin.H{
"response": map[string]string{ "target_origin": redirectURI,
"error": "response_type is invalid", "authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "response_type is invalid",
},
}, },
}, })
}) }
return return
} }
if isResponseTypeCode { if isResponseTypeCode {
if codeChallenge == "" { if codeChallenge == "" {
gc.HTML(http.StatusBadRequest, template, gin.H{ if isQuery {
"target_origin": redirectURI, gc.Redirect(http.StatusFound, loginURL)
"authorization_response": map[string]interface{}{ } else {
"type": "authorization_response", gc.HTML(http.StatusBadRequest, template, gin.H{
"response": map[string]string{ "target_origin": redirectURI,
"error": "code_challenge is required", "authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": map[string]string{
"error": "code_challenge is required",
},
}, },
}, })
}) }
return return
} }
} }
sessionToken, err := cookie.GetSession(gc) sessionToken, err := cookie.GetSession(gc)
if err != nil { if err != nil {
gc.HTML(http.StatusOK, template, gin.H{ if isQuery {
"target_origin": redirectURI, gc.Redirect(http.StatusFound, loginURL)
"authorization_response": map[string]interface{}{ } else {
"type": "authorization_response", gc.HTML(http.StatusOK, template, gin.H{
"response": map[string]string{ "target_origin": redirectURI,
"error": "login_required", "authorization_response": map[string]interface{}{
"error_description": "Login is required", "type": "authorization_response",
"response": map[string]string{
"error": "login_required",
"error_description": "Login is required",
},
}, },
}, })
}) }
return return
} }
// get session from cookie // get session from cookie
claims, err := token.ValidateBrowserSession(gc, sessionToken) claims, err := token.ValidateBrowserSession(gc, sessionToken)
if err != nil { if err != nil {
gc.HTML(http.StatusOK, template, gin.H{ if isQuery {
"target_origin": redirectURI, gc.Redirect(http.StatusFound, loginURL)
"authorization_response": map[string]interface{}{ } else {
"type": "authorization_response", gc.HTML(http.StatusOK, template, gin.H{
"response": map[string]string{ "target_origin": redirectURI,
"error": "login_required", "authorization_response": map[string]interface{}{
"error_description": "Login is required", "type": "authorization_response",
"response": map[string]string{
"error": "login_required",
"error_description": "Login is required",
},
}, },
}, })
}) }
return return
} }
userID := claims.Subject userID := claims.Subject
user, err := db.Provider.GetUserByID(userID) user, err := db.Provider.GetUserByID(userID)
if err != nil { if err != nil {
gc.HTML(http.StatusOK, template, gin.H{ if isQuery {
"target_origin": redirectURI, gc.Redirect(http.StatusFound, loginURL)
"authorization_response": map[string]interface{}{ } else {
"type": "authorization_response", gc.HTML(http.StatusOK, template, gin.H{
"response": map[string]string{ "target_origin": redirectURI,
"error": "signup_required", "authorization_response": map[string]interface{}{
"error_description": "Sign up required", "type": "authorization_response",
"response": map[string]string{
"error": "signup_required",
"error_description": "Sign up required",
},
}, },
}, })
}) }
return return
} }
@@ -180,16 +218,20 @@ func AuthorizeHandler() gin.HandlerFunc {
nonce := uuid.New().String() nonce := uuid.New().String()
newSessionTokenData, newSessionToken, err := token.CreateSessionToken(user, nonce, claims.Roles, scope) newSessionTokenData, newSessionToken, err := token.CreateSessionToken(user, nonce, claims.Roles, scope)
if err != nil { if err != nil {
gc.HTML(http.StatusOK, template, gin.H{ if isQuery {
"target_origin": redirectURI, gc.Redirect(http.StatusFound, loginURL)
"authorization_response": map[string]interface{}{ } else {
"type": "authorization_response", gc.HTML(http.StatusOK, template, gin.H{
"response": map[string]string{ "target_origin": redirectURI,
"error": "login_required", "authorization_response": map[string]interface{}{
"error_description": "Login is required", "type": "authorization_response",
"response": map[string]string{
"error": "login_required",
"error_description": "Login is required",
},
}, },
}, })
}) }
return return
} }
@@ -214,24 +256,31 @@ func AuthorizeHandler() gin.HandlerFunc {
// rollover the session for security // rollover the session for security
authToken, err := token.CreateAuthToken(gc, user, claims.Roles, scope) authToken, err := token.CreateAuthToken(gc, user, claims.Roles, scope)
if err != nil { if err != nil {
gc.HTML(http.StatusOK, template, gin.H{ if isQuery {
"target_origin": redirectURI, gc.Redirect(http.StatusFound, loginURL)
"authorization_response": map[string]interface{}{ } else {
"type": "authorization_response", gc.HTML(http.StatusOK, template, gin.H{
"response": map[string]string{ "target_origin": redirectURI,
"error": "login_required", "authorization_response": map[string]interface{}{
"error_description": "Login is required", "type": "authorization_response",
"response": map[string]string{
"error": "login_required",
"error_description": "Login is required",
},
}, },
}, })
}) }
return return
} }
sessionstore.RemoveState(sessionToken) sessionstore.RemoveState(sessionToken)
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
cookie.SetSession(gc, authToken.FingerPrintHash) cookie.SetSession(gc, authToken.FingerPrintHash)
expiresIn := int64(1800) expiresIn := int64(1800)
// used of query mode
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token
res := map[string]interface{}{ res := map[string]interface{}{
"access_token": authToken.AccessToken.Token, "access_token": authToken.AccessToken.Token,
"id_token": authToken.IDToken.Token, "id_token": authToken.IDToken.Token,
@@ -243,29 +292,42 @@ func AuthorizeHandler() gin.HandlerFunc {
if authToken.RefreshToken != nil { if authToken.RefreshToken != nil {
res["refresh_token"] = authToken.RefreshToken.Token res["refresh_token"] = authToken.RefreshToken.Token
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) params += "&refresh_token=" + authToken.RefreshToken.Token
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
} }
gc.HTML(http.StatusOK, template, gin.H{ if isQuery {
"target_origin": redirectURI, if strings.Contains(redirectURI, "?") {
"authorization_response": map[string]interface{}{ gc.Redirect(http.StatusFound, redirectURI+"&"+params)
"type": "authorization_response", } else {
"response": res, gc.Redirect(http.StatusFound, redirectURI+"?"+params)
}, }
}) } else {
gc.HTML(http.StatusOK, template, gin.H{
"target_origin": redirectURI,
"authorization_response": map[string]interface{}{
"type": "authorization_response",
"response": res,
},
})
}
return return
} }
// by default return with error if isQuery {
gc.HTML(http.StatusOK, template, gin.H{ gc.Redirect(http.StatusFound, loginURL)
"target_origin": redirectURI, } else {
"authorization_response": map[string]interface{}{ // by default return with error
"type": "authorization_response", gc.HTML(http.StatusOK, template, gin.H{
"response": map[string]string{ "target_origin": redirectURI,
"error": "login_required", "authorization_response": map[string]interface{}{
"error_description": "Login is required", "type": "authorization_response",
"response": map[string]string{
"error": "login_required",
"error_description": "Login is required",
},
}, },
}, })
}) }
} }
} }

View File

@@ -2,6 +2,7 @@ package handlers
import ( import (
"net/http" "net/http"
"strings"
"github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/crypto" "github.com/authorizerdev/authorizer/server/crypto"
@@ -9,8 +10,10 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// Handler to logout user
func LogoutHandler() gin.HandlerFunc { func LogoutHandler() gin.HandlerFunc {
return func(gc *gin.Context) { return func(gc *gin.Context) {
redirectURL := strings.TrimSpace(gc.Query("redirect_uri"))
// get fingerprint hash // get fingerprint hash
fingerprintHash, err := cookie.GetSession(gc) fingerprintHash, err := cookie.GetSession(gc)
if err != nil { if err != nil {
@@ -33,8 +36,12 @@ func LogoutHandler() gin.HandlerFunc {
sessionstore.RemoveState(fingerPrint) sessionstore.RemoveState(fingerPrint)
cookie.DeleteSession(gc) cookie.DeleteSession(gc)
gc.JSON(http.StatusOK, gin.H{ if redirectURL != "" {
"message": "Logged out successfully", gc.Redirect(http.StatusFound, redirectURL)
}) } else {
gc.JSON(http.StatusOK, gin.H{
"message": "Logged out successfully",
})
}
} }
} }

View File

@@ -7,6 +7,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
@@ -21,7 +22,6 @@ import (
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@@ -39,14 +39,15 @@ func OAuthCallbackHandler() gin.HandlerFunc {
// contains random token, redirect url, role // contains random token, redirect url, role
sessionSplit := strings.Split(state, "___") sessionSplit := strings.Split(state, "___")
// TODO validate redirect url if len(sessionSplit) < 3 {
if len(sessionSplit) < 2 {
c.JSON(400, gin.H{"error": "invalid redirect url"}) c.JSON(400, gin.H{"error": "invalid redirect url"})
return return
} }
inputRoles := strings.Split(sessionSplit[2], ",") stateValue := sessionSplit[0]
redirectURL := sessionSplit[1] redirectURL := sessionSplit[1]
inputRoles := strings.Split(sessionSplit[2], ",")
scopes := strings.Split(sessionSplit[3], ",")
var err error var err error
user := models.User{} user := models.User{}
@@ -145,17 +146,29 @@ func OAuthCallbackHandler() gin.HandlerFunc {
} }
} }
// TODO use query param authToken, err := token.CreateAuthToken(c, user, inputRoles, scopes)
scope := []string{"openid", "email", "profile"}
nonce := uuid.New().String()
_, newSessionToken, err := token.CreateSessionToken(user, nonce, inputRoles, scope)
if err != nil { if err != nil {
c.JSON(500, gin.H{"error": err.Error()}) c.JSON(500, gin.H{"error": err.Error()})
} }
expiresIn := int64(1800)
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + stateValue + "&id_token=" + authToken.IDToken.Token
cookie.SetSession(c, authToken.FingerPrintHash)
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
if authToken.RefreshToken != nil {
params = params + `&refresh_token=` + authToken.RefreshToken.Token
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
}
sessionstore.SetState(newSessionToken, nonce+"@"+user.ID)
cookie.SetSession(c, newSessionToken)
go utils.SaveSessionInDB(c, user.ID) go utils.SaveSessionInDB(c, user.ID)
if strings.Contains(redirectURL, "?") {
redirectURL = redirectURL + "&" + params
} else {
redirectURL = redirectURL + "?" + params
}
c.Redirect(http.StatusTemporaryRedirect, redirectURL) c.Redirect(http.StatusTemporaryRedirect, redirectURL)
} }
} }

View File

@@ -10,23 +10,38 @@ import (
"github.com/authorizerdev/authorizer/server/sessionstore" "github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
) )
// OAuthLoginHandler set host in the oauth state that is useful for redirecting to oauth_callback // OAuthLoginHandler set host in the oauth state that is useful for redirecting to oauth_callback
func OAuthLoginHandler() gin.HandlerFunc { func OAuthLoginHandler() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
hostname := utils.GetHost(c) hostname := utils.GetHost(c)
redirectURL := c.Query("redirectURL") redirectURI := strings.TrimSpace(c.Query("redirectURL"))
roles := c.Query("roles") roles := strings.TrimSpace(c.Query("roles"))
state := strings.TrimSpace(c.Query("state"))
scopeString := strings.TrimSpace(c.Query("scope"))
if redirectURL == "" { if redirectURI == "" {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"error": "invalid redirect url", "error": "invalid redirect uri",
}) })
return return
} }
if state == "" {
c.JSON(400, gin.H{
"error": "invalid state",
})
return
}
var scope []string
if scopeString == "" {
scope = []string{"openid", "profile", "email"}
} else {
scope = strings.Split(scopeString, " ")
}
if roles != "" { if roles != "" {
// validate role // validate role
rolesSplit := strings.Split(roles, ",") rolesSplit := strings.Split(roles, ",")
@@ -43,8 +58,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",") roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
} }
uuid := uuid.New() oauthStateString := state + "___" + redirectURI + "___" + roles + "___" + strings.Join(scope, ",")
oauthStateString := uuid.String() + "___" + redirectURL + "___" + roles
provider := c.Param("oauth_provider") provider := c.Param("oauth_provider")
isProviderConfigured := true isProviderConfigured := true

50
server/handlers/revoke.go Normal file
View File

@@ -0,0 +1,50 @@
package handlers
import (
"net/http"
"strings"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/envstore"
"github.com/authorizerdev/authorizer/server/sessionstore"
"github.com/gin-gonic/gin"
)
// Revoke handler to revoke refresh token
func RevokeHandler() gin.HandlerFunc {
return func(gc *gin.Context) {
var reqBody map[string]string
if err := gc.BindJSON(&reqBody); err != nil {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "error_binding_json",
"error_description": err.Error(),
})
return
}
// get fingerprint hash
refreshToken := strings.TrimSpace(reqBody["refresh_token"])
clientID := strings.TrimSpace(reqBody["client_id"])
if clientID == "" {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "client_id_required",
"error_description": "The client id is required",
})
return
}
if clientID != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_client_id",
"error_description": "The client id is invalid",
})
return
}
sessionstore.RemoveState(refreshToken)
gc.JSON(http.StatusOK, gin.H{
"message": "Token revoked successfully",
})
}
}

View File

@@ -15,6 +15,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// TokenHandler to handle /oauth/token requests
// grant type required
func TokenHandler() gin.HandlerFunc { func TokenHandler() gin.HandlerFunc {
return func(gc *gin.Context) { return func(gc *gin.Context) {
var reqBody map[string]string var reqBody map[string]string
@@ -29,6 +31,22 @@ func TokenHandler() gin.HandlerFunc {
codeVerifier := strings.TrimSpace(reqBody["code_verifier"]) codeVerifier := strings.TrimSpace(reqBody["code_verifier"])
code := strings.TrimSpace(reqBody["code"]) code := strings.TrimSpace(reqBody["code"])
clientID := strings.TrimSpace(reqBody["client_id"]) clientID := strings.TrimSpace(reqBody["client_id"])
grantType := strings.TrimSpace(reqBody["grant_type"])
refreshToken := strings.TrimSpace(reqBody["refresh_token"])
if grantType == "" {
grantType = "authorization_code"
}
isRefreshTokenGrant := grantType == "refresh_token"
isAuthorizationCodeGrant := grantType == "authorization_code"
if !isRefreshTokenGrant && !isAuthorizationCodeGrant {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_grant_type",
"error_description": "grant_type is invalid",
})
}
if clientID == "" { if clientID == "" {
gc.JSON(http.StatusBadRequest, gin.H{ gc.JSON(http.StatusBadRequest, gin.H{
@@ -46,58 +64,95 @@ func TokenHandler() gin.HandlerFunc {
return return
} }
if codeVerifier == "" { var userID string
gc.JSON(http.StatusBadRequest, gin.H{ var roles, scope []string
"error": "invalid_code_verifier", if isAuthorizationCodeGrant {
"error_description": "The code verifier is required",
}) if codeVerifier == "" {
return gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier",
"error_description": "The code verifier is required",
})
return
}
if code == "" {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code",
"error_description": "The code is required",
})
return
}
hash := sha256.New()
hash.Write([]byte(codeVerifier))
encryptedCode := strings.ReplaceAll(base64.URLEncoding.EncodeToString(hash.Sum(nil)), "+", "-")
encryptedCode = strings.ReplaceAll(encryptedCode, "/", "_")
encryptedCode = strings.ReplaceAll(encryptedCode, "=", "")
sessionData := sessionstore.GetState(encryptedCode)
if sessionData == "" {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier",
"error_description": "The code verifier is invalid",
})
return
}
// split session data
// it contains code@sessiontoken
sessionDataSplit := strings.Split(sessionData, "@")
if sessionDataSplit[0] != code {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier",
"error_description": "The code verifier is invalid",
})
return
}
// rollover the session for security
sessionstore.RemoveState(sessionDataSplit[1])
// validate session
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
if err != nil {
gc.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"error_description": "Invalid session data",
})
return
}
userID = claims.Subject
roles = claims.Roles
scope = claims.Scope
} else {
// validate refresh token
if refreshToken == "" {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_refresh_token",
"error_description": "The refresh token is invalid",
})
}
claims, err := token.ValidateRefreshToken(gc, refreshToken)
if err != nil {
gc.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"error_description": err.Error(),
})
}
userID = claims["sub"].(string)
rolesInterface := claims["roles"].([]interface{})
scopeInterface := claims["scope"].([]interface{})
for _, v := range rolesInterface {
roles = append(roles, v.(string))
}
for _, v := range scopeInterface {
scope = append(scope, v.(string))
}
// remove older refresh token and rotate it for security
sessionstore.RemoveState(refreshToken)
} }
if code == "" {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code",
"error_description": "The code is required",
})
return
}
hash := sha256.New()
hash.Write([]byte(codeVerifier))
encryptedCode := strings.ReplaceAll(base64.URLEncoding.EncodeToString(hash.Sum(nil)), "+", "-")
encryptedCode = strings.ReplaceAll(encryptedCode, "/", "_")
encryptedCode = strings.ReplaceAll(encryptedCode, "=", "")
sessionData := sessionstore.GetState(encryptedCode)
if sessionData == "" {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier",
"error_description": "The code verifier is invalid",
})
return
}
// split session data
// it contains code@sessiontoken
sessionDataSplit := strings.Split(sessionData, "@")
if sessionDataSplit[0] != code {
gc.JSON(http.StatusBadRequest, gin.H{
"error": "invalid_code_verifier",
"error_description": "The code verifier is invalid",
})
return
}
// validate session
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
if err != nil {
gc.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized",
"error_description": "Invalid session data",
})
return
}
userID := claims.Subject
user, err := db.Provider.GetUserByID(userID) user, err := db.Provider.GetUserByID(userID)
if err != nil { if err != nil {
gc.JSON(http.StatusUnauthorized, gin.H{ gc.JSON(http.StatusUnauthorized, gin.H{
@@ -106,9 +161,8 @@ func TokenHandler() gin.HandlerFunc {
}) })
return return
} }
// rollover the session for security
sessionstore.RemoveState(sessionDataSplit[1]) authToken, err := token.CreateAuthToken(gc, user, roles, scope)
authToken, err := token.CreateAuthToken(gc, user, claims.Roles, claims.Scope)
if err != nil { if err != nil {
gc.JSON(http.StatusUnauthorized, gin.H{ gc.JSON(http.StatusUnauthorized, gin.H{
"error": "unauthorized", "error": "unauthorized",
@@ -124,13 +178,14 @@ func TokenHandler() gin.HandlerFunc {
res := map[string]interface{}{ res := map[string]interface{}{
"access_token": authToken.AccessToken.Token, "access_token": authToken.AccessToken.Token,
"id_token": authToken.IDToken.Token, "id_token": authToken.IDToken.Token,
"scope": claims.Scope, "scope": scope,
"roles": roles,
"expires_in": expiresIn, "expires_in": expiresIn,
} }
if authToken.RefreshToken != nil { if authToken.RefreshToken != nil {
res["refresh_token"] = authToken.RefreshToken.Token res["refresh_token"] = authToken.RefreshToken.Token
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
} }
gc.JSON(http.StatusOK, res) gc.JSON(http.StatusOK, res)

View File

@@ -2,6 +2,7 @@ package handlers
import ( import (
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
@@ -11,7 +12,6 @@ import (
"github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/token"
"github.com/authorizerdev/authorizer/server/utils" "github.com/authorizerdev/authorizer/server/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
) )
// VerifyEmailHandler handles the verify email route. // VerifyEmailHandler handles the verify email route.
@@ -19,7 +19,7 @@ import (
func VerifyEmailHandler() gin.HandlerFunc { func VerifyEmailHandler() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
errorRes := gin.H{ errorRes := gin.H{
"error": "invalid token", "error": "invalid_token",
} }
tokenInQuery := c.Query("token") tokenInQuery := c.Query("token")
if tokenInQuery == "" { if tokenInQuery == "" {
@@ -29,30 +29,24 @@ func VerifyEmailHandler() gin.HandlerFunc {
verificationRequest, err := db.Provider.GetVerificationRequestByToken(tokenInQuery) verificationRequest, err := db.Provider.GetVerificationRequestByToken(tokenInQuery)
if err != nil { if err != nil {
errorRes["error_description"] = err.Error()
c.JSON(400, errorRes) c.JSON(400, errorRes)
return return
} }
// verify if token exists in db // verify if token exists in db
hostname := utils.GetHost(c) hostname := utils.GetHost(c)
encryptedNonce, err := utils.EncryptNonce(verificationRequest.Nonce) claim, err := token.ParseJWTToken(tokenInQuery, hostname, verificationRequest.Nonce, verificationRequest.Email)
if err != nil {
c.JSON(400, gin.H{
"error": err.Error(),
})
return
}
claim, err := token.ParseJWTToken(tokenInQuery, hostname, encryptedNonce, verificationRequest.Email)
if err != nil { if err != nil {
errorRes["error_description"] = err.Error()
c.JSON(400, errorRes) c.JSON(400, errorRes)
return return
} }
user, err := db.Provider.GetUserByEmail(claim["sub"].(string)) user, err := db.Provider.GetUserByEmail(claim["sub"].(string))
if err != nil { if err != nil {
c.JSON(400, gin.H{ errorRes["error_description"] = err.Error()
"message": err.Error(), c.JSON(400, errorRes)
})
return return
} }
@@ -65,21 +59,53 @@ func VerifyEmailHandler() gin.HandlerFunc {
// delete from verification table // delete from verification table
db.Provider.DeleteVerificationRequest(verificationRequest) db.Provider.DeleteVerificationRequest(verificationRequest)
roles := strings.Split(user.Roles, ",") state := strings.TrimSpace(c.Query("state"))
scope := []string{"openid", "email", "profile"} redirectURL := strings.TrimSpace(c.Query("redirect_uri"))
nonce := uuid.New().String() rolesString := strings.TrimSpace(c.Query("roles"))
_, authToken, err := token.CreateSessionToken(user, nonce, roles, scope) var roles []string
if rolesString == "" {
roles = strings.Split(user.Roles, ",")
} else {
roles = strings.Split(rolesString, ",")
}
scopeString := strings.TrimSpace(c.Query("scope"))
var scope []string
if scopeString == "" {
scope = []string{"openid", "email", "profile"}
} else {
scope = strings.Split(scopeString, " ")
}
authToken, err := token.CreateAuthToken(c, user, roles, scope)
if err != nil { if err != nil {
c.JSON(400, gin.H{ errorRes["error_description"] = err.Error()
"message": err.Error(), c.JSON(500, errorRes)
})
return return
} }
sessionstore.SetState(authToken, nonce+"@"+user.ID) expiresIn := int64(1800)
cookie.SetSession(c, authToken) params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token
cookie.SetSession(c, authToken.FingerPrintHash)
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
if authToken.RefreshToken != nil {
params = params + `&refresh_token=${refresh_token}`
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
}
if redirectURL == "" {
redirectURL = claim["redirect_uri"].(string)
}
if strings.Contains(redirectURL, "?") {
redirectURL = redirectURL + "&" + params
} else {
redirectURL = redirectURL + "?" + params
}
go utils.SaveSessionInDB(c, user.ID) go utils.SaveSessionInDB(c, user.ID)
c.Redirect(http.StatusTemporaryRedirect, claim["redirect_url"].(string)) c.Redirect(http.StatusTemporaryRedirect, redirectURL)
} }
} }

View File

@@ -39,20 +39,26 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
} }
hostname := utils.GetHost(gc) hostname := utils.GetHost(gc)
nonce, nonceHash, err := utils.GenerateNonce() _, nonceHash, err := utils.GenerateNonce()
if err != nil { if err != nil {
return res, err return res, err
} }
verificationToken, err := token.CreateVerificationToken(params.Email, constants.VerificationTypeForgotPassword, hostname, nonceHash) redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
if params.RedirectURI != nil {
redirectURL = *params.RedirectURI
}
verificationToken, err := token.CreateVerificationToken(params.Email, constants.VerificationTypeForgotPassword, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: verificationToken, Token: verificationToken,
Identifier: constants.VerificationTypeForgotPassword, Identifier: constants.VerificationTypeForgotPassword,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
Nonce: nonce, Nonce: nonceHash,
RedirectURI: redirectURL,
}) })
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency

View File

@@ -69,8 +69,6 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
return res, err return res, err
} }
cookie.SetSession(gc, authToken.FingerPrintHash)
expiresIn := int64(1800) expiresIn := int64(1800)
res = &model.AuthResponse{ res = &model.AuthResponse{
Message: `Logged in successfully`, Message: `Logged in successfully`,
@@ -80,12 +78,13 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
User: user.AsAPIUser(), User: user.AsAPIUser(),
} }
cookie.SetSession(gc, authToken.FingerPrintHash)
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
if authToken.RefreshToken != nil { if authToken.RefreshToken != nil {
res.RefreshToken = &authToken.RefreshToken.Token res.RefreshToken = &authToken.RefreshToken.Token
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
} }
go utils.SaveSessionInDB(gc, user.ID) go utils.SaveSessionInDB(gc, user.ID)

View File

@@ -68,6 +68,9 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
// Need to modify roles in this case // Need to modify roles in this case
// find the unassigned roles // find the unassigned roles
if len(params.Roles) <= 0 {
inputRoles = envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles)
}
existingRoles := strings.Split(existingUser.Roles, ",") existingRoles := strings.Split(existingUser.Roles, ",")
unasignedRoles := []string{} unasignedRoles := []string{}
for _, ir := range inputRoles { for _, ir := range inputRoles {
@@ -109,24 +112,46 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
hostname := utils.GetHost(gc) hostname := utils.GetHost(gc)
if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) { if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
// insert verification request // insert verification request
nonce, nonceHash, err := utils.GenerateNonce() _, nonceHash, err := utils.GenerateNonce()
if err != nil { if err != nil {
return res, err return res, err
} }
redirectURLParams := "&roles=" + strings.Join(inputRoles, ",")
if params.State != nil {
redirectURLParams = redirectURLParams + "&state=" + *params.State
}
if params.Scope != nil && len(params.Scope) > 0 {
redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ")
}
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
if params.RedirectURI != nil {
redirectURL = *params.RedirectURI
}
if strings.Contains(redirectURL, "?") {
redirectURL = redirectURL + "&" + redirectURLParams
} else {
redirectURL = redirectURL + "?" + redirectURLParams
}
verificationType := constants.VerificationTypeMagicLinkLogin verificationType := constants.VerificationTypeMagicLinkLogin
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash) verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ _, err = db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: verificationToken, Token: verificationToken,
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
Nonce: nonce, Nonce: nonceHash,
RedirectURI: redirectURL,
}) })
if err != nil {
return res, err
}
// exec it as go routin so that we can reduce the api latency // exec it as go routing so that we can reduce the api latency
go email.SendVerificationMail(params.Email, verificationToken, hostname) go email.SendVerificationMail(params.Email, verificationToken, hostname)
} }

View File

@@ -44,20 +44,22 @@ func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEma
} }
hostname := utils.GetHost(gc) hostname := utils.GetHost(gc)
nonce, nonceHash, err := utils.GenerateNonce() _, nonceHash, err := utils.GenerateNonce()
if err != nil { if err != nil {
return res, err return res, err
} }
verificationToken, err := token.CreateVerificationToken(params.Email, params.Identifier, hostname, nonceHash)
verificationToken, err := token.CreateVerificationToken(params.Email, params.Identifier, hostname, nonceHash, verificationRequest.RedirectURI)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: verificationToken, Token: verificationToken,
Identifier: params.Identifier, Identifier: params.Identifier,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
Nonce: nonce, Nonce: nonceHash,
RedirectURI: verificationRequest.RedirectURI,
}) })
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency

View File

@@ -37,11 +37,7 @@ func ResetPasswordResolver(ctx context.Context, params model.ResetPasswordInput)
// verify if token exists in db // verify if token exists in db
hostname := utils.GetHost(gc) hostname := utils.GetHost(gc)
encryptedNonce, err := utils.EncryptNonce(verificationRequest.Nonce) claim, err := token.ParseJWTToken(params.Token, hostname, verificationRequest.Nonce, verificationRequest.Email)
if err != nil {
return res, err
}
claim, err := token.ParseJWTToken(params.Token, hostname, encryptedNonce, verificationRequest.Email)
if err != nil { if err != nil {
return res, fmt.Errorf(`invalid token`) return res, fmt.Errorf(`invalid token`)
} }

View File

@@ -0,0 +1,16 @@
package resolvers
import (
"context"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/sessionstore"
)
// RevokeResolver resolver to revoke refresh token
func RevokeResolver(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) {
sessionstore.RemoveState(params.RefreshToken)
return &model.Response{
Message: "Token revoked",
}, nil
}

View File

@@ -80,7 +80,7 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod
if authToken.RefreshToken != nil { if authToken.RefreshToken != nil {
res.RefreshToken = &authToken.RefreshToken.Token res.RefreshToken = &authToken.RefreshToken.Token
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
} }
return res, nil return res, nil

View File

@@ -123,21 +123,23 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
hostname := utils.GetHost(gc) hostname := utils.GetHost(gc)
if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) { if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
// insert verification request // insert verification request
nonce, nonceHash, err := utils.GenerateNonce() _, nonceHash, err := utils.GenerateNonce()
if err != nil { if err != nil {
return res, err return res, err
} }
verificationType := constants.VerificationTypeBasicAuthSignup verificationType := constants.VerificationTypeBasicAuthSignup
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash) redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
return res, err return res, err
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: verificationToken, Token: verificationToken,
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: params.Email, Email: params.Email,
Nonce: nonce, Nonce: nonceHash,
RedirectURI: redirectURL,
}) })
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency

View File

@@ -129,21 +129,23 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
user.EmailVerifiedAt = nil user.EmailVerifiedAt = nil
hasEmailChanged = true hasEmailChanged = true
// insert verification request // insert verification request
nonce, nonceHash, err := utils.GenerateNonce() _, nonceHash, err := utils.GenerateNonce()
if err != nil { if err != nil {
return res, err return res, err
} }
verificationType := constants.VerificationTypeUpdateEmail verificationType := constants.VerificationTypeUpdateEmail
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash) redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: verificationToken, Token: verificationToken,
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: newEmail, Email: newEmail,
Nonce: nonce, Nonce: nonceHash,
RedirectURI: redirectURL,
}) })
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency

View File

@@ -101,21 +101,23 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
user.Email = newEmail user.Email = newEmail
user.EmailVerifiedAt = nil user.EmailVerifiedAt = nil
// insert verification request // insert verification request
nonce, nonceHash, err := utils.GenerateNonce() _, nonceHash, err := utils.GenerateNonce()
if err != nil { if err != nil {
return res, err return res, err
} }
verificationType := constants.VerificationTypeUpdateEmail verificationType := constants.VerificationTypeUpdateEmail
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash) redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
if err != nil { if err != nil {
log.Println(`error generating token`, err) log.Println(`error generating token`, err)
} }
db.Provider.AddVerificationRequest(models.VerificationRequest{ db.Provider.AddVerificationRequest(models.VerificationRequest{
Token: verificationToken, Token: verificationToken,
Identifier: verificationType, Identifier: verificationType,
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
Email: newEmail, Email: newEmail,
Nonce: nonce, Nonce: nonceHash,
RedirectURI: redirectURL,
}) })
// exec it as go routin so that we can reduce the api latency // exec it as go routin so that we can reduce the api latency

View File

@@ -29,11 +29,7 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m
// verify if token exists in db // verify if token exists in db
hostname := utils.GetHost(gc) hostname := utils.GetHost(gc)
encryptedNonce, err := utils.EncryptNonce(verificationRequest.Nonce) claim, err := token.ParseJWTToken(params.Token, hostname, verificationRequest.Nonce, verificationRequest.Email)
if err != nil {
return res, err
}
claim, err := token.ParseJWTToken(params.Token, hostname, encryptedNonce, verificationRequest.Email)
if err != nil { if err != nil {
return res, fmt.Errorf(`invalid token: %s`, err.Error()) return res, fmt.Errorf(`invalid token: %s`, err.Error())
} }

View File

@@ -27,6 +27,7 @@ func InitRouter() *gin.Engine {
router.GET("/userinfo", handlers.UserInfoHandler()) router.GET("/userinfo", handlers.UserInfoHandler())
router.GET("/logout", handlers.LogoutHandler()) router.GET("/logout", handlers.LogoutHandler())
router.POST("/oauth/token", handlers.TokenHandler()) router.POST("/oauth/token", handlers.TokenHandler())
router.POST("/oauth/revoke", handlers.RevokeHandler())
router.LoadHTMLGlob("templates/*") router.LoadHTMLGlob("templates/*")
// login page app related routes. // login page app related routes.

View File

@@ -91,7 +91,7 @@ func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string) (
} }
if utils.StringSliceContains(scope, "offline_access") { if utils.StringSliceContains(scope, "offline_access") {
refreshToken, refreshTokenExpiresAt, err := CreateRefreshToken(user, roles, hostname, nonce) refreshToken, refreshTokenExpiresAt, err := CreateRefreshToken(user, roles, scope, hostname, nonce)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -103,7 +103,7 @@ func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string) (
} }
// CreateRefreshToken util to create JWT token // CreateRefreshToken util to create JWT token
func CreateRefreshToken(user models.User, roles []string, hostname, nonce string) (string, int64, error) { func CreateRefreshToken(user models.User, roles, scopes []string, hostname, nonce string) (string, int64, error) {
// expires in 1 year // expires in 1 year
expiryBound := time.Hour * 8760 expiryBound := time.Hour * 8760
expiresAt := time.Now().Add(expiryBound).Unix() expiresAt := time.Now().Add(expiryBound).Unix()
@@ -115,6 +115,7 @@ func CreateRefreshToken(user models.User, roles []string, hostname, nonce string
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
"token_type": constants.TokenTypeRefreshToken, "token_type": constants.TokenTypeRefreshToken,
"roles": roles, "roles": roles,
"scope": scopes,
"nonce": nonce, "nonce": nonce,
} }
@@ -198,6 +199,36 @@ func ValidateAccessToken(gc *gin.Context, accessToken string) (map[string]interf
return res, nil return res, nil
} }
// Function to validate refreshToken
func ValidateRefreshToken(gc *gin.Context, refreshToken string) (map[string]interface{}, error) {
var res map[string]interface{}
if refreshToken == "" {
return res, fmt.Errorf(`unauthorized`)
}
savedSession := sessionstore.GetState(refreshToken)
if savedSession == "" {
return res, fmt.Errorf(`unauthorized`)
}
savedSessionSplit := strings.Split(savedSession, "@")
nonce := savedSessionSplit[0]
userID := savedSessionSplit[1]
hostname := utils.GetHost(gc)
res, err := ParseJWTToken(refreshToken, hostname, nonce, userID)
if err != nil {
return res, err
}
if res["token_type"] != constants.TokenTypeRefreshToken {
return res, fmt.Errorf(`unauthorized: invalid token type`)
}
return res, nil
}
func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionData, error) { func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionData, error) {
if encryptedSession == "" { if encryptedSession == "" {
return nil, fmt.Errorf(`unauthorized`) return nil, fmt.Errorf(`unauthorized`)

View File

@@ -2,6 +2,7 @@ package token
import ( import (
"errors" "errors"
"fmt"
"github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/crypto" "github.com/authorizerdev/authorizer/server/crypto"
@@ -91,6 +92,7 @@ func ParseJWTToken(token, hostname, nonce, subject string) (jwt.MapClaims, error
return claims, errors.New("invalid audience") return claims, errors.New("invalid audience")
} }
fmt.Println("claims:", claims["nonce"], nonce, claims["nonce"] == nonce)
if claims["nonce"] != nonce { if claims["nonce"] != nonce {
return claims, errors.New("invalid nonce") return claims, errors.New("invalid nonce")
} }

View File

@@ -9,7 +9,7 @@ import (
) )
// CreateVerificationToken creates a verification JWT token // CreateVerificationToken creates a verification JWT token
func CreateVerificationToken(email, tokenType, hostname, nonceHash string) (string, error) { func CreateVerificationToken(email, tokenType, hostname, nonceHash, redirectURL string) (string, error) {
claims := jwt.MapClaims{ claims := jwt.MapClaims{
"iss": hostname, "iss": hostname,
"aud": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID), "aud": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID),
@@ -18,7 +18,7 @@ func CreateVerificationToken(email, tokenType, hostname, nonceHash string) (stri
"iat": time.Now().Unix(), "iat": time.Now().Unix(),
"token_type": tokenType, "token_type": tokenType,
"nonce": nonceHash, "nonce": nonceHash,
"redirect_url": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL), "redirect_uri": redirectURL,
} }
return SignJWTToken(claims) return SignJWTToken(claims)

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>{{.data.organizationName}}</title> <title>{{.data.organizationName}}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="apple-touch-icon" sizes="180x180" href="/app/favicon_io/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/app/favicon_io/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/app/favicon_io/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/app/favicon_io/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/app/favicon_io/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/app/favicon_io/favicon-16x16.png">

View File

@@ -8,7 +8,6 @@
(function (window, document) { (function (window, document) {
var targetOrigin = {{.target_origin}}; var targetOrigin = {{.target_origin}};
var authorizationResponse = {{.authorization_response}}; var authorizationResponse = {{.authorization_response}};
console.log({targetOrigin})
window.parent.postMessage(authorizationResponse, targetOrigin); window.parent.postMessage(authorizationResponse, targetOrigin);
})(this, this.document); })(this, this.document);
</script> </script>