From 0787a3b49485873d266c680348813b5fbaec143c Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Fri, 4 Mar 2022 12:56:11 +0530 Subject: [PATCH] feat: add token endpoint --- server/cookie/cookie.go | 7 +- server/handlers/authorize.go | 39 +++++++---- server/handlers/token.go | 126 +++++++++++++++++++++++++++++++++++ server/routes/routes.go | 1 + 4 files changed, 159 insertions(+), 14 deletions(-) create mode 100644 server/handlers/token.go diff --git a/server/cookie/cookie.go b/server/cookie/cookie.go index 20103b0..54600af 100644 --- a/server/cookie/cookie.go +++ b/server/cookie/cookie.go @@ -2,6 +2,7 @@ package cookie import ( "net/http" + "net/url" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/envstore" @@ -56,5 +57,9 @@ func GetSession(gc *gin.Context) (string, error) { } } - return cookie.Value, nil + decodedValue, err := url.PathUnescape(cookie.Value) + if err != nil { + return "", err + } + return decodedValue, nil } diff --git a/server/handlers/authorize.go b/server/handlers/authorize.go index 402f64a..3450a02 100644 --- a/server/handlers/authorize.go +++ b/server/handlers/authorize.go @@ -1,7 +1,6 @@ package handlers import ( - "fmt" "net/http" "strings" @@ -27,6 +26,8 @@ func AuthorizeHandler() gin.HandlerFunc { responseType := strings.TrimSpace(gc.Query("response_type")) state := strings.TrimSpace(gc.Query("state")) codeChallenge := strings.TrimSpace(gc.Query("code_challenge")) + scopeString := strings.TrimSpace(gc.Query("scope")) + scope := []string{} template := "authorize.tmpl" if redirectURI == "" { @@ -59,6 +60,10 @@ func AuthorizeHandler() gin.HandlerFunc { responseType = "token" } + if scopeString == "" { + scope = []string{"openid", "profile", "email"} + } + isResponseTypeCode := responseType == "code" isResponseTypeToken := responseType == "token" @@ -142,7 +147,7 @@ func AuthorizeHandler() gin.HandlerFunc { // rollover the session for security sessionstore.RemoveState(sessionToken) nonce := uuid.New().String() - newSessionTokenData, newSessionToken, err := token.CreateSessionToken(user, nonce, claims.Roles, claims.Scope) + newSessionTokenData, newSessionToken, err := token.CreateSessionToken(user, nonce, claims.Roles, scope) if err != nil { gc.HTML(http.StatusOK, template, gin.H{ "target_origin": nil, @@ -160,7 +165,7 @@ func AuthorizeHandler() gin.HandlerFunc { sessionstore.SetState(newSessionToken, newSessionTokenData.Nonce+"@"+user.ID) cookie.SetSession(gc, newSessionToken) code := uuid.New().String() - sessionstore.SetState("code_challenge_"+codeChallenge, code) + sessionstore.SetState(codeChallenge, code+"@"+newSessionToken) gc.HTML(http.StatusOK, template, gin.H{ "target_origin": redirectURI, "authorization_response": map[string]string{ @@ -173,7 +178,7 @@ func AuthorizeHandler() gin.HandlerFunc { if isResponseTypeToken { // rollover the session for security - authToken, err := token.CreateAuthToken(gc, user, claims.Roles, claims.Scope) + authToken, err := token.CreateAuthToken(gc, user, claims.Roles, scope) if err != nil { gc.HTML(http.StatusOK, template, gin.H{ "target_origin": nil, @@ -191,20 +196,28 @@ 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) + res := map[string]interface{}{ + "access_token": authToken.AccessToken.Token, + "id_token": authToken.IDToken.Token, + "state": state, + "scope": scope, + "token_type": "Bearer", + "expires_in": expiresIn, + } + + if authToken.RefreshToken != nil { + res["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{}{ - "access_token": authToken.AccessToken.Token, - "id_token": authToken.IDToken.Token, - "state": state, - "scope": claims.Scope, - "expires_in": expiresIn, - }, + "target_origin": redirectURI, + "authorization_response": res, }) return } - fmt.Println("=> returning from here...") // by default return with error gc.HTML(http.StatusOK, template, gin.H{ diff --git a/server/handlers/token.go b/server/handlers/token.go new file mode 100644 index 0000000..095dcd1 --- /dev/null +++ b/server/handlers/token.go @@ -0,0 +1,126 @@ +package handlers + +import ( + "crypto/sha256" + "encoding/base64" + "net/http" + "strings" + + "github.com/authorizerdev/authorizer/server/cookie" + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/sessionstore" + "github.com/authorizerdev/authorizer/server/token" + "github.com/gin-gonic/gin" +) + +func TokenHandler() 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 + } + + codeVerifier := strings.TrimSpace(reqBody["code_verifier"]) + code := strings.TrimSpace(reqBody["code"]) + redirectURI := strings.TrimSpace(reqBody["redirect_uri"]) + + if codeVerifier == "" { + 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 + } + + if redirectURI == "" { + gc.JSON(http.StatusBadRequest, gin.H{ + "error": "invalid_redirect_uri", + "error_description": "The redirect URI is required", + }) + return + } + + hash := sha256.New() + hash.Write([]byte(codeVerifier)) + encryptedCode := strings.TrimSuffix(base64.URLEncoding.EncodeToString(hash.Sum(nil)), "=") + 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) + if err != nil { + gc.JSON(http.StatusUnauthorized, gin.H{ + "error": "unauthorized", + "error_description": "User not found", + }) + return + } + // rollover the session for security + sessionstore.RemoveState(sessionDataSplit[1]) + authToken, err := token.CreateAuthToken(gc, user, claims.Roles, claims.Scope) + if err != nil { + gc.JSON(http.StatusUnauthorized, gin.H{ + "error": "unauthorized", + "error_description": "User not found", + }) + return + } + 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) + res := map[string]interface{}{ + "access_token": authToken.AccessToken.Token, + "id_token": authToken.IDToken.Token, + "scope": claims.Scope, + "expires_in": expiresIn, + } + + if authToken.RefreshToken != nil { + res["refresh_token"] = authToken.RefreshToken.Token + sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID) + } + + gc.JSON(http.StatusOK, res) + } +} diff --git a/server/routes/routes.go b/server/routes/routes.go index a8599fc..89b6073 100644 --- a/server/routes/routes.go +++ b/server/routes/routes.go @@ -26,6 +26,7 @@ func InitRouter() *gin.Engine { router.GET("/authorize", handlers.AuthorizeHandler()) router.GET("/userinfo", handlers.UserInfoHandler()) router.GET("/logout", handlers.LogoutHandler()) + router.POST("/token", handlers.TokenHandler()) router.LoadHTMLGlob("templates/*") // login page app related routes.