fix: add token information in redirect url

This commit is contained in:
Lakhan Samani 2022-03-08 12:36:26 +05:30
parent 57bc091499
commit 8c2bf6ee0d
26 changed files with 440 additions and 225 deletions

30
app/package-lock.json generated
View File

@ -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"

View File

@ -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",

View File

@ -6,9 +6,6 @@ import Root from './Root';
export default function App() {
// @ts-ignore
const globalState: Record<string, string> = window['__authorizer__'];
if (globalState.state) {
sessionStorage.setItem('authorizer_state', globalState.state);
}
return (
<div
style={{
@ -33,15 +30,7 @@ export default function App() {
/>
<h1>{globalState.organizationName}</h1>
</div>
<div
style={{
width: 400,
margin: `10px auto`,
border: `1px solid #D1D5DB`,
padding: `25px 20px`,
borderRadius: 5,
}}
>
<div className="container">
<BrowserRouter>
<AuthorizerProvider
config={{

View File

@ -11,14 +11,19 @@ export default function Root() {
useEffect(() => {
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 () => {};

View File

@ -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;
}
}

View File

@ -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,
}
}

View File

@ -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))
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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),
},

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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`)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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())
}

View File

@ -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")
}

View File

@ -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)

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<title>{{.data.organizationName}}</title>
<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="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">