diff --git a/app/package-lock.json b/app/package-lock.json index 99dba0f..71db036 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "0.8.0", + "@authorizerdev/authorizer-react": "0.9.0-beta.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", @@ -24,9 +24,9 @@ } }, "node_modules/@authorizerdev/authorizer-js": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.3.0.tgz", - "integrity": "sha512-KCE5Dw5MUnEgstBUayBriDQAOjqbxU7ixC00rTHAE6aD6TxJkeSls0vCTXpvt4iiKhFK6q9BhHwa/5NwWYpDBQ==", + "version": "0.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.0.tgz", + "integrity": "sha512-wNh5ROldNqdbOXFPDlq1tObzPZyEQkbnOvSEwvnDfPYb9/BsJ3naj3/ayz4J2R5k2+Eyuk0LK64XYdkfIW0HYA==", "dependencies": { "node-fetch": "^2.6.1" }, @@ -35,11 +35,11 @@ } }, "node_modules/@authorizerdev/authorizer-react": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.8.0.tgz", - "integrity": "sha512-178XWGEPsovy3f6Yi2Llh6kFmjdf3ZrkIsqIAKEGPhZawV/1sA6v+4FZp7ReuCxsCelckFFQUnPR8P7od+2HeA==", + "version": "0.9.0-beta.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.0.tgz", + "integrity": "sha512-+I0JGobxuTDwKPAqwkJuczXnQ+ooNnz9IrfQPl8ZHnKoZsRqSSPCOLO6o0BLBu65h6pWZdLEEFN8CjRnu1+Zuw==", "dependencies": { - "@authorizerdev/authorizer-js": "^0.3.0", + "@authorizerdev/authorizer-js": "^0.4.0-beta.0", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" @@ -829,19 +829,19 @@ }, "dependencies": { "@authorizerdev/authorizer-js": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.3.0.tgz", - "integrity": "sha512-KCE5Dw5MUnEgstBUayBriDQAOjqbxU7ixC00rTHAE6aD6TxJkeSls0vCTXpvt4iiKhFK6q9BhHwa/5NwWYpDBQ==", + "version": "0.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.0.tgz", + "integrity": "sha512-wNh5ROldNqdbOXFPDlq1tObzPZyEQkbnOvSEwvnDfPYb9/BsJ3naj3/ayz4J2R5k2+Eyuk0LK64XYdkfIW0HYA==", "requires": { "node-fetch": "^2.6.1" } }, "@authorizerdev/authorizer-react": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.8.0.tgz", - "integrity": "sha512-178XWGEPsovy3f6Yi2Llh6kFmjdf3ZrkIsqIAKEGPhZawV/1sA6v+4FZp7ReuCxsCelckFFQUnPR8P7od+2HeA==", + "version": "0.9.0-beta.0", + "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.0.tgz", + "integrity": "sha512-+I0JGobxuTDwKPAqwkJuczXnQ+ooNnz9IrfQPl8ZHnKoZsRqSSPCOLO6o0BLBu65h6pWZdLEEFN8CjRnu1+Zuw==", "requires": { - "@authorizerdev/authorizer-js": "^0.3.0", + "@authorizerdev/authorizer-js": "^0.4.0-beta.0", "final-form": "^4.20.2", "react-final-form": "^6.5.3", "styled-components": "^5.3.0" diff --git a/app/package.json b/app/package.json index cd974d6..69baeba 100644 --- a/app/package.json +++ b/app/package.json @@ -11,7 +11,7 @@ "author": "Lakhan Samani", "license": "ISC", "dependencies": { - "@authorizerdev/authorizer-react": "latest", + "@authorizerdev/authorizer-react": "0.9.0-beta.0", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", "esbuild": "^0.12.17", diff --git a/app/src/App.tsx b/app/src/App.tsx index 58ec66a..284e3ad 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -6,9 +6,6 @@ import Root from './Root'; export default function App() { // @ts-ignore const globalState: Record = window['__authorizer__']; - if (globalState.state) { - sessionStorage.setItem('authorizer_state', globalState.state); - } return (

{globalState.organizationName}

-
+
{ if (token) { - const state = sessionStorage.getItem('authorizer_state')?.trim(); - 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) { - console.log({ x: `${config.redirectURL || '/app'}?state=${state}` }); sessionStorage.removeItem('authorizer_state'); - window.location.replace( - `${config.redirectURL || '/app'}?state=${state}` - ); + window.location.replace(redirectURL); } } return () => {}; diff --git a/app/src/index.css b/app/src/index.css index 151b3d2..c86863c 100644 --- a/app/src/index.css +++ b/app/src/index.css @@ -1,5 +1,5 @@ body { - margin: 0; + margin: 10; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; @@ -14,3 +14,17 @@ body { *:after { 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; + } +} diff --git a/server/db/models/verification_requests.go b/server/db/models/verification_requests.go index eec5427..50fa77f 100644 --- a/server/db/models/verification_requests.go +++ b/server/db/models/verification_requests.go @@ -4,25 +4,28 @@ import "github.com/authorizerdev/authorizer/server/graph/model" // VerificationRequest model for db type VerificationRequest struct { - Key string `json:"_key,omitempty" bson:"_key"` // for arangodb - ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"` - Token string `gorm:"type:text" json:"token" bson:"token"` - Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"` - ExpiresAt int64 `json:"expires_at" bson:"expires_at"` - CreatedAt int64 `json:"created_at" bson:"created_at"` - UpdatedAt int64 `json:"updated_at" bson:"updated_at"` - Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"` - Nonce string `gorm:"type:char(36)" json:"nonce" bson:"nonce"` + Key string `json:"_key,omitempty" bson:"_key"` // for arangodb + ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"` + Token string `gorm:"type:text" json:"token" bson:"token"` + Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"` + ExpiresAt int64 `json:"expires_at" bson:"expires_at"` + CreatedAt int64 `json:"created_at" bson:"created_at"` + UpdatedAt int64 `json:"updated_at" bson:"updated_at"` + Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"` + Nonce string `gorm:"type:char(36)" json:"nonce" bson:"nonce"` + RedirectURI string `gorm:"type:text" json:"redirect_uri" bson:"redirect_uri"` } func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest { return &model.VerificationRequest{ - ID: v.ID, - Token: &v.Token, - Identifier: &v.Identifier, - Expires: &v.ExpiresAt, - CreatedAt: &v.CreatedAt, - UpdatedAt: &v.UpdatedAt, - Email: &v.Email, + ID: v.ID, + Token: &v.Token, + Identifier: &v.Identifier, + Expires: &v.ExpiresAt, + CreatedAt: &v.CreatedAt, + UpdatedAt: &v.UpdatedAt, + Email: &v.Email, + Nonce: &v.Nonce, + RedirectURI: &v.RedirectURI, } } diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 69b021f..ff0175d 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -173,13 +173,15 @@ type ComplexityRoot struct { } VerificationRequest struct { - CreatedAt func(childComplexity int) int - Email func(childComplexity int) int - Expires func(childComplexity int) int - ID func(childComplexity int) int - Identifier func(childComplexity int) int - Token func(childComplexity int) int - UpdatedAt func(childComplexity int) int + CreatedAt func(childComplexity int) int + Email func(childComplexity int) int + Expires func(childComplexity int) int + ID func(childComplexity int) int + Identifier func(childComplexity int) int + Nonce func(childComplexity int) int + RedirectURI func(childComplexity int) int + Token func(childComplexity int) int + UpdatedAt func(childComplexity int) int } VerificationRequests struct { @@ -1038,6 +1040,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in 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": if e.complexity.VerificationRequest.Token == nil { break @@ -1189,6 +1205,8 @@ type VerificationRequest { expires: Int64 created_at: Int64 updated_at: Int64 + nonce: String + redirect_uri: String } type VerificationRequests { @@ -1361,6 +1379,8 @@ input UpdateUserInput { input ForgotPasswordInput { email: String! + state: String + redirect_uri: String } input ResetPasswordInput { @@ -1377,6 +1397,8 @@ input MagicLinkLoginInput { email: String! roles: [String!] scope: [String!] + state: String + redirect_uri: String } input SessionQueryInput { @@ -5451,6 +5473,70 @@ func (ec *executionContext) _VerificationRequest_updated_at(ctx context.Context, 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) { defer func() { if r := recover(); r != nil { @@ -6729,6 +6815,22 @@ func (ec *executionContext) unmarshalInputForgotPasswordInput(ctx context.Contex if err != nil { return it, err } + case "state": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state")) + it.State, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + 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 +6917,22 @@ func (ec *executionContext) unmarshalInputMagicLinkLoginInput(ctx context.Contex if err != nil { return it, err } + case "state": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state")) + it.State, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + 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 + } } } @@ -8290,6 +8408,10 @@ func (ec *executionContext) _VerificationRequest(ctx context.Context, sel ast.Se out.Values[i] = ec._VerificationRequest_created_at(ctx, field, obj) case "updated_at": 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: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 5134dda..2825956 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -69,7 +69,9 @@ type Error struct { } type ForgotPasswordInput struct { - Email string `json:"email"` + Email string `json:"email"` + State *string `json:"state"` + RedirectURI *string `json:"redirect_uri"` } type LoginInput struct { @@ -80,9 +82,11 @@ type LoginInput struct { } type MagicLinkLoginInput struct { - Email string `json:"email"` - Roles []string `json:"roles"` - Scope []string `json:"scope"` + Email string `json:"email"` + Roles []string `json:"roles"` + Scope []string `json:"scope"` + State *string `json:"state"` + RedirectURI *string `json:"redirect_uri"` } type Meta struct { @@ -239,13 +243,15 @@ type Users struct { } type VerificationRequest struct { - ID string `json:"id"` - Identifier *string `json:"identifier"` - Token *string `json:"token"` - Email *string `json:"email"` - Expires *int64 `json:"expires"` - CreatedAt *int64 `json:"created_at"` - UpdatedAt *int64 `json:"updated_at"` + ID string `json:"id"` + Identifier *string `json:"identifier"` + Token *string `json:"token"` + Email *string `json:"email"` + Expires *int64 `json:"expires"` + CreatedAt *int64 `json:"created_at"` + UpdatedAt *int64 `json:"updated_at"` + Nonce *string `json:"nonce"` + RedirectURI *string `json:"redirect_uri"` } type VerificationRequests struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index cf01374..b1549d2 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -57,6 +57,8 @@ type VerificationRequest { expires: Int64 created_at: Int64 updated_at: Int64 + nonce: String + redirect_uri: String } type VerificationRequests { @@ -229,6 +231,8 @@ input UpdateUserInput { input ForgotPasswordInput { email: String! + state: String + redirect_uri: String } input ResetPasswordInput { @@ -245,6 +249,8 @@ input MagicLinkLoginInput { email: String! roles: [String!] scope: [String!] + state: String + redirect_uri: String } input SessionQueryInput { diff --git a/server/handlers/app.go b/server/handlers/app.go index 06cad5b..9300eda 100644 --- a/server/handlers/app.go +++ b/server/handlers/app.go @@ -1,13 +1,11 @@ package handlers import ( - "encoding/json" "log" "net/http" "strings" "github.com/authorizerdev/authorizer/server/constants" - "github.com/authorizerdev/authorizer/server/crypto" "github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/utils" "github.com/gin-gonic/gin" @@ -18,7 +16,6 @@ import ( type State struct { AuthorizerURL string `json:"authorizerURL"` RedirectURL string `json:"redirectURL"` - State string `json:"state"` } // AppHandler is the handler for the /app route @@ -30,44 +27,25 @@ func AppHandler() gin.HandlerFunc { 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 - - if state == "" { - stateObj.AuthorizerURL = hostname - stateObj.RedirectURL = hostname + "/app" + var scope []string + if scopeString == "" { + scope = []string{"openid", "profile", "email"} } else { - decodedState, err := crypto.DecryptB64(state) - 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, "/") + scope = strings.Split(scopeString, " ") + } + if redirect_uri == "" { + redirect_uri = hostname + "/app" + } else { // 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"}) 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 @@ -78,10 +56,11 @@ func AppHandler() gin.HandlerFunc { } } c.HTML(http.StatusOK, "app.tmpl", gin.H{ - "data": map[string]string{ - "authorizerURL": stateObj.AuthorizerURL, - "redirectURL": stateObj.RedirectURL, - "state": stateObj.State, + "data": map[string]interface{}{ + "authorizerURL": hostname, + "redirectURL": redirect_uri, + "scope": scope, + "state": state, "organizationName": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName), "organizationLogo": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo), }, diff --git a/server/handlers/authorize.go b/server/handlers/authorize.go index 1ef6fbc..c9b5a46 100644 --- a/server/handlers/authorize.go +++ b/server/handlers/authorize.go @@ -2,16 +2,15 @@ package handlers import ( "net/http" + "strconv" "strings" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie" - "github.com/authorizerdev/authorizer/server/crypto" "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/envstore" "github.com/authorizerdev/authorizer/server/sessionstore" "github.com/authorizerdev/authorizer/server/token" - "github.com/authorizerdev/authorizer/server/utils" "github.com/gin-gonic/gin" "github.com/google/uuid" ) @@ -36,6 +35,13 @@ func AuthorizeHandler() gin.HandlerFunc { template := "authorize.tmpl" responseMode := strings.TrimSpace(gc.Query("response_mode")) + var scope []string + if scopeString == "" { + scope = []string{"openid", "profile", "email"} + } else { + scope = strings.Split(scopeString, " ") + } + if responseMode == "" { responseMode = "query" } @@ -50,9 +56,7 @@ func AuthorizeHandler() gin.HandlerFunc { isQuery := responseMode == "query" - hostname := utils.GetHost(gc) - loginRedirectState := crypto.EncryptB64(`{"authorizerURL":"` + hostname + `","redirectURL":"` + redirectURI + `", "state":"` + state + `"}`) - loginURL := "/app?state=" + loginRedirectState + loginURL := "/app?state=" + state + "&scope=" + strings.Join(scope, " ") + "&redirect_uri=" + redirectURI if clientID == "" { if isQuery { @@ -109,13 +113,6 @@ func AuthorizeHandler() gin.HandlerFunc { responseType = "token" } - var scope []string - if scopeString == "" { - scope = []string{"openid", "profile", "email"} - } else { - scope = strings.Split(scopeString, " ") - } - isResponseTypeCode := responseType == "code" isResponseTypeToken := responseType == "token" @@ -279,8 +276,11 @@ func AuthorizeHandler() gin.HandlerFunc { sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) cookie.SetSession(gc, authToken.FingerPrintHash) - 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{}{ "access_token": authToken.AccessToken.Token, "id_token": authToken.IDToken.Token, @@ -292,16 +292,25 @@ func AuthorizeHandler() gin.HandlerFunc { if authToken.RefreshToken != nil { res["refresh_token"] = authToken.RefreshToken.Token + params += "&refresh_token=" + authToken.RefreshToken.Token sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) } - gc.HTML(http.StatusOK, template, gin.H{ - "target_origin": redirectURI, - "authorization_response": map[string]interface{}{ - "type": "authorization_response", - "response": res, - }, - }) + if isQuery { + if strings.Contains(redirectURI, "?") { + gc.Redirect(http.StatusFound, redirectURI+"&"+params) + } else { + 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 } diff --git a/server/handlers/oauth_callback.go b/server/handlers/oauth_callback.go index 81aea1e..9b76ba9 100644 --- a/server/handlers/oauth_callback.go +++ b/server/handlers/oauth_callback.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "log" "net/http" + "strconv" "strings" "time" @@ -21,7 +22,6 @@ import ( "github.com/authorizerdev/authorizer/server/utils" "github.com/coreos/go-oidc/v3/oidc" "github.com/gin-gonic/gin" - "github.com/google/uuid" "golang.org/x/oauth2" ) @@ -37,16 +37,17 @@ func OAuthCallbackHandler() gin.HandlerFunc { } sessionstore.GetState(state) // contains random token, redirect url, role - sessionSplit := strings.Split(state, "___") + sessionSplit := strings.Split(state, "@") - // TODO validate redirect url - if len(sessionSplit) < 2 { + if len(sessionSplit) < 3 { c.JSON(400, gin.H{"error": "invalid redirect url"}) return } - inputRoles := strings.Split(sessionSplit[2], ",") + stateValue := sessionSplit[0] redirectURL := sessionSplit[1] + inputRoles := strings.Split(sessionSplit[2], ",") + scopes := strings.Split(sessionSplit[3], ",") var err error user := models.User{} @@ -145,17 +146,29 @@ func OAuthCallbackHandler() gin.HandlerFunc { } } - // TODO use query param - scope := []string{"openid", "email", "profile"} - nonce := uuid.New().String() - _, newSessionToken, err := token.CreateSessionToken(user, nonce, inputRoles, scope) + authToken, err := token.CreateAuthToken(c, user, inputRoles, scopes) if err != nil { 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=${refresh_token}` + sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) + } - sessionstore.SetState(newSessionToken, nonce+"@"+user.ID) - cookie.SetSession(c, newSessionToken) go utils.SaveSessionInDB(c, user.ID) + if strings.Contains(redirectURL, "?") { + redirectURL = redirectURL + "&" + params + } else { + redirectURL = redirectURL + "?" + params + } + c.Redirect(http.StatusTemporaryRedirect, redirectURL) } } diff --git a/server/handlers/oauth_login.go b/server/handlers/oauth_login.go index 9ef000e..4f5e5dd 100644 --- a/server/handlers/oauth_login.go +++ b/server/handlers/oauth_login.go @@ -10,23 +10,38 @@ import ( "github.com/authorizerdev/authorizer/server/sessionstore" "github.com/authorizerdev/authorizer/server/utils" "github.com/gin-gonic/gin" - "github.com/google/uuid" ) // OAuthLoginHandler set host in the oauth state that is useful for redirecting to oauth_callback func OAuthLoginHandler() gin.HandlerFunc { return func(c *gin.Context) { hostname := utils.GetHost(c) - redirectURL := c.Query("redirectURL") - roles := c.Query("roles") + redirectURI := strings.TrimSpace(c.Query("redirectURL")) + 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{ - "error": "invalid redirect url", + "error": "invalid redirect uri", }) 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 != "" { // validate role rolesSplit := strings.Split(roles, ",") @@ -43,8 +58,7 @@ func OAuthLoginHandler() gin.HandlerFunc { roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",") } - uuid := uuid.New() - oauthStateString := uuid.String() + "___" + redirectURL + "___" + roles + oauthStateString := state + "@" + redirectURI + "@" + roles + "@" + strings.Join(scope, ",") provider := c.Param("oauth_provider") isProviderConfigured := true diff --git a/server/handlers/verify_email.go b/server/handlers/verify_email.go index acc5dea..99e7e68 100644 --- a/server/handlers/verify_email.go +++ b/server/handlers/verify_email.go @@ -2,6 +2,7 @@ package handlers import ( "net/http" + "strconv" "strings" "time" @@ -11,7 +12,6 @@ import ( "github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/utils" "github.com/gin-gonic/gin" - "github.com/google/uuid" ) // VerifyEmailHandler handles the verify email route. @@ -19,7 +19,7 @@ import ( func VerifyEmailHandler() gin.HandlerFunc { return func(c *gin.Context) { errorRes := gin.H{ - "error": "invalid token", + "error": "invalid_token", } tokenInQuery := c.Query("token") if tokenInQuery == "" { @@ -29,30 +29,24 @@ func VerifyEmailHandler() gin.HandlerFunc { verificationRequest, err := db.Provider.GetVerificationRequestByToken(tokenInQuery) if err != nil { + errorRes["error_description"] = err.Error() c.JSON(400, errorRes) return } // verify if token exists in db hostname := utils.GetHost(c) - encryptedNonce, err := utils.EncryptNonce(verificationRequest.Nonce) - if err != nil { - c.JSON(400, gin.H{ - "error": err.Error(), - }) - return - } - claim, err := token.ParseJWTToken(tokenInQuery, hostname, encryptedNonce, verificationRequest.Email) + claim, err := token.ParseJWTToken(tokenInQuery, hostname, verificationRequest.Nonce, verificationRequest.Email) if err != nil { + errorRes["error_description"] = err.Error() c.JSON(400, errorRes) return } user, err := db.Provider.GetUserByEmail(claim["sub"].(string)) if err != nil { - c.JSON(400, gin.H{ - "message": err.Error(), - }) + errorRes["error_description"] = err.Error() + c.JSON(400, errorRes) return } @@ -65,21 +59,53 @@ func VerifyEmailHandler() gin.HandlerFunc { // delete from verification table db.Provider.DeleteVerificationRequest(verificationRequest) - roles := strings.Split(user.Roles, ",") - scope := []string{"openid", "email", "profile"} - nonce := uuid.New().String() - _, authToken, err := token.CreateSessionToken(user, nonce, roles, scope) + state := strings.TrimSpace(c.Query("state")) + redirectURL := strings.TrimSpace(c.Query("redirect_uri")) + rolesString := strings.TrimSpace(c.Query("roles")) + 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 { - c.JSON(400, gin.H{ - "message": err.Error(), - }) + errorRes["error_description"] = err.Error() + c.JSON(500, errorRes) return } - sessionstore.SetState(authToken, nonce+"@"+user.ID) - cookie.SetSession(c, authToken) + expiresIn := int64(1800) + 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.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) + } + + if redirectURL == "" { + redirectURL = claim["redirect_url"].(string) + } + + if strings.Contains(redirectURL, "?") { + redirectURL = redirectURL + "&" + params + } else { + redirectURL = redirectURL + "?" + params + } go utils.SaveSessionInDB(c, user.ID) - c.Redirect(http.StatusTemporaryRedirect, claim["redirect_url"].(string)) + c.Redirect(http.StatusTemporaryRedirect, redirectURL) } } diff --git a/server/resolvers/forgot_password.go b/server/resolvers/forgot_password.go index 49eaa75..4be96b1 100644 --- a/server/resolvers/forgot_password.go +++ b/server/resolvers/forgot_password.go @@ -39,20 +39,26 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu } hostname := utils.GetHost(gc) - nonce, nonceHash, err := utils.GenerateNonce() + _, nonceHash, err := utils.GenerateNonce() if err != nil { 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 { log.Println(`error generating token`, err) } db.Provider.AddVerificationRequest(models.VerificationRequest{ - Token: verificationToken, - Identifier: constants.VerificationTypeForgotPassword, - ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), - Email: params.Email, - Nonce: nonce, + Token: verificationToken, + Identifier: constants.VerificationTypeForgotPassword, + ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), + Email: params.Email, + Nonce: nonceHash, + RedirectURI: redirectURL, }) // exec it as go routin so that we can reduce the api latency diff --git a/server/resolvers/login.go b/server/resolvers/login.go index 03ffbb5..5a099b5 100644 --- a/server/resolvers/login.go +++ b/server/resolvers/login.go @@ -69,8 +69,6 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes return res, err } - cookie.SetSession(gc, authToken.FingerPrintHash) - expiresIn := int64(1800) res = &model.AuthResponse{ Message: `Logged in successfully`, @@ -80,6 +78,7 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes User: user.AsAPIUser(), } + cookie.SetSession(gc, authToken.FingerPrintHash) sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID) sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) diff --git a/server/resolvers/magic_link_login.go b/server/resolvers/magic_link_login.go index a5d80ed..ff39da8 100644 --- a/server/resolvers/magic_link_login.go +++ b/server/resolvers/magic_link_login.go @@ -68,6 +68,9 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu // Need to modify roles in this case // find the unassigned roles + if len(params.Roles) <= 0 { + inputRoles = envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles) + } existingRoles := strings.Split(existingUser.Roles, ",") unasignedRoles := []string{} for _, ir := range inputRoles { @@ -109,21 +112,40 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu hostname := utils.GetHost(gc) if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) { // insert verification request - nonce, nonceHash, err := utils.GenerateNonce() + _, nonceHash, err := utils.GenerateNonce() if err != nil { 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 - verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash) + verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL) if err != nil { log.Println(`error generating token`, err) } db.Provider.AddVerificationRequest(models.VerificationRequest{ - Token: verificationToken, - Identifier: verificationType, - ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), - Email: params.Email, - Nonce: nonce, + Token: verificationToken, + Identifier: verificationType, + ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), + Email: params.Email, + Nonce: nonceHash, + RedirectURI: redirectURL, }) // exec it as go routin so that we can reduce the api latency diff --git a/server/resolvers/resend_verify_email.go b/server/resolvers/resend_verify_email.go index f514681..68cb502 100644 --- a/server/resolvers/resend_verify_email.go +++ b/server/resolvers/resend_verify_email.go @@ -44,20 +44,22 @@ func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEma } hostname := utils.GetHost(gc) - nonce, nonceHash, err := utils.GenerateNonce() + _, nonceHash, err := utils.GenerateNonce() if err != nil { 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 { log.Println(`error generating token`, err) } db.Provider.AddVerificationRequest(models.VerificationRequest{ - Token: verificationToken, - Identifier: params.Identifier, - ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), - Email: params.Email, - Nonce: nonce, + Token: verificationToken, + Identifier: params.Identifier, + ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), + Email: params.Email, + Nonce: nonceHash, + RedirectURI: verificationRequest.RedirectURI, }) // exec it as go routin so that we can reduce the api latency diff --git a/server/resolvers/reset_password.go b/server/resolvers/reset_password.go index d94707f..5a498c8 100644 --- a/server/resolvers/reset_password.go +++ b/server/resolvers/reset_password.go @@ -37,11 +37,7 @@ func ResetPasswordResolver(ctx context.Context, params model.ResetPasswordInput) // verify if token exists in db hostname := utils.GetHost(gc) - encryptedNonce, err := utils.EncryptNonce(verificationRequest.Nonce) - if err != nil { - return res, err - } - claim, err := token.ParseJWTToken(params.Token, hostname, encryptedNonce, verificationRequest.Email) + claim, err := token.ParseJWTToken(params.Token, hostname, verificationRequest.Nonce, verificationRequest.Email) if err != nil { return res, fmt.Errorf(`invalid token`) } diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index 69e57c3..2f15f15 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -123,21 +123,23 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR hostname := utils.GetHost(gc) if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) { // insert verification request - nonce, nonceHash, err := utils.GenerateNonce() + _, nonceHash, err := utils.GenerateNonce() if err != nil { return res, err } 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 { return res, err } db.Provider.AddVerificationRequest(models.VerificationRequest{ - Token: verificationToken, - Identifier: verificationType, - ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), - Email: params.Email, - Nonce: nonce, + Token: verificationToken, + Identifier: verificationType, + ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), + Email: params.Email, + Nonce: nonceHash, + RedirectURI: redirectURL, }) // exec it as go routin so that we can reduce the api latency diff --git a/server/resolvers/update_profile.go b/server/resolvers/update_profile.go index cc43f4c..5a3f328 100644 --- a/server/resolvers/update_profile.go +++ b/server/resolvers/update_profile.go @@ -129,21 +129,23 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput) user.EmailVerifiedAt = nil hasEmailChanged = true // insert verification request - nonce, nonceHash, err := utils.GenerateNonce() + _, nonceHash, err := utils.GenerateNonce() if err != nil { return res, err } 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 { log.Println(`error generating token`, err) } db.Provider.AddVerificationRequest(models.VerificationRequest{ - Token: verificationToken, - Identifier: verificationType, - ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), - Email: newEmail, - Nonce: nonce, + Token: verificationToken, + Identifier: verificationType, + ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), + Email: newEmail, + Nonce: nonceHash, + RedirectURI: redirectURL, }) // exec it as go routin so that we can reduce the api latency diff --git a/server/resolvers/update_user.go b/server/resolvers/update_user.go index 948565e..f9438d4 100644 --- a/server/resolvers/update_user.go +++ b/server/resolvers/update_user.go @@ -101,21 +101,23 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod user.Email = newEmail user.EmailVerifiedAt = nil // insert verification request - nonce, nonceHash, err := utils.GenerateNonce() + _, nonceHash, err := utils.GenerateNonce() if err != nil { return res, err } 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 { log.Println(`error generating token`, err) } db.Provider.AddVerificationRequest(models.VerificationRequest{ - Token: verificationToken, - Identifier: verificationType, - ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), - Email: newEmail, - Nonce: nonce, + Token: verificationToken, + Identifier: verificationType, + ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), + Email: newEmail, + Nonce: nonceHash, + RedirectURI: redirectURL, }) // exec it as go routin so that we can reduce the api latency diff --git a/server/resolvers/verify_email.go b/server/resolvers/verify_email.go index 953db47..fe13c9a 100644 --- a/server/resolvers/verify_email.go +++ b/server/resolvers/verify_email.go @@ -29,11 +29,7 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m // verify if token exists in db hostname := utils.GetHost(gc) - encryptedNonce, err := utils.EncryptNonce(verificationRequest.Nonce) - if err != nil { - return res, err - } - claim, err := token.ParseJWTToken(params.Token, hostname, encryptedNonce, verificationRequest.Email) + claim, err := token.ParseJWTToken(params.Token, hostname, verificationRequest.Nonce, verificationRequest.Email) if err != nil { return res, fmt.Errorf(`invalid token: %s`, err.Error()) } diff --git a/server/token/jwt.go b/server/token/jwt.go index 90f6333..912f1e5 100644 --- a/server/token/jwt.go +++ b/server/token/jwt.go @@ -2,6 +2,7 @@ package token import ( "errors" + "fmt" "github.com/authorizerdev/authorizer/server/constants" "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") } + fmt.Println("claims:", claims, claims["nonce"], nonce) if claims["nonce"] != nonce { return claims, errors.New("invalid nonce") } diff --git a/server/token/verification_token.go b/server/token/verification_token.go index 7765392..71114d7 100644 --- a/server/token/verification_token.go +++ b/server/token/verification_token.go @@ -9,7 +9,7 @@ import ( ) // 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{ "iss": hostname, "aud": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID), @@ -18,7 +18,7 @@ func CreateVerificationToken(email, tokenType, hostname, nonceHash string) (stri "iat": time.Now().Unix(), "token_type": tokenType, "nonce": nonceHash, - "redirect_url": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL), + "redirect_url": redirectURL, } return SignJWTToken(claims) diff --git a/templates/app.tmpl b/templates/app.tmpl index 58f0159..bd15a4e 100644 --- a/templates/app.tmpl +++ b/templates/app.tmpl @@ -4,7 +4,7 @@ {{.data.organizationName}} - +