diff --git a/server/crypto/common.go b/server/crypto/common.go index df3754c..7bd3513 100644 --- a/server/crypto/common.go +++ b/server/crypto/common.go @@ -99,7 +99,7 @@ func EncryptEnvData(data envstore.Store) (string, error) { return "", err } - return EncryptB64(string(encryptedConfig)), nil + return string(encryptedConfig), nil } // EncryptPassword is used for encrypting password diff --git a/server/handlers/authorize.go b/server/handlers/authorize.go index 67123cd..402f64a 100644 --- a/server/handlers/authorize.go +++ b/server/handlers/authorize.go @@ -5,7 +5,12 @@ import ( "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" + "github.com/google/uuid" ) // AuthorizeHandler is the handler for the /authorize route @@ -14,59 +19,203 @@ import ( // state[recommended] = to prevent CSRF attack (for authorizer its compulsory) // code_challenge = to prevent CSRF attack // code_challenge_method = to prevent CSRF attack [only sh256 is supported] + +// check the flow for generating and verifying codes: https://developer.okta.com/blog/2019/08/22/okta-authjs-pkce#:~:text=PKCE%20works%20by%20having%20the,is%20called%20the%20Code%20Challenge. func AuthorizeHandler() gin.HandlerFunc { - return func(c *gin.Context) { - redirectURI := strings.TrimSpace(c.Query("redirect_uri")) - responseType := strings.TrimSpace(c.Query("response_type")) - state := strings.TrimSpace(c.Query("state")) - codeChallenge := strings.TrimSpace(c.Query("code_challenge")) - codeChallengeMethod := strings.TrimSpace(c.Query("code_challenge_method")) - fmt.Println(codeChallengeMethod) + return func(gc *gin.Context) { + redirectURI := strings.TrimSpace(gc.Query("redirect_uri")) + responseType := strings.TrimSpace(gc.Query("response_type")) + state := strings.TrimSpace(gc.Query("state")) + codeChallenge := strings.TrimSpace(gc.Query("code_challenge")) template := "authorize.tmpl" if redirectURI == "" { - c.HTML(http.StatusBadRequest, template, gin.H{ - "targetOrigin": nil, - "authorizationResponse": nil, - "error": "redirect_uri is required", + gc.HTML(http.StatusOK, template, gin.H{ + "target_origin": nil, + "authorization_response": map[string]interface{}{ + "type": "authorization_response", + "response": map[string]string{ + "error": "redirect_uri is required", + }, + }, }) return } if state == "" { - c.HTML(http.StatusBadRequest, template, gin.H{ - "targetOrigin": nil, - "authorizationResponse": nil, - "error": "state is required", + gc.HTML(http.StatusOK, template, gin.H{ + "target_origin": nil, + "authorization_response": map[string]interface{}{ + "type": "authorization_response", + "response": map[string]string{ + "error": "state is required", + }, + }, }) return } if responseType == "" { - responseType = "code" + responseType = "token" } - isCode := responseType == "code" - isToken := responseType == "token" + isResponseTypeCode := responseType == "code" + isResponseTypeToken := responseType == "token" - if !isCode && !isToken { - c.HTML(http.StatusBadRequest, template, gin.H{ - "targetOrigin": nil, - "authorizationResponse": nil, - "error": "response_type is invalid", + if !isResponseTypeCode && !isResponseTypeToken { + gc.HTML(http.StatusOK, template, gin.H{ + "target_origin": nil, + "authorization_response": map[string]interface{}{ + "type": "authorization_response", + "response": map[string]string{ + "error": "response_type is invalid", + }, + }, }) return } - if isCode { + if isResponseTypeCode { if codeChallenge == "" { - c.HTML(http.StatusBadRequest, template, gin.H{ - "targetOrigin": nil, - "authorizationResponse": nil, - "error": "code_challenge is required", + gc.HTML(http.StatusBadRequest, template, gin.H{ + "target_origin": nil, + "authorization_response": map[string]interface{}{ + "type": "authorization_response", + "response": map[string]string{ + "error": "code_challenge is required", + }, + }, }) return } } + + sessionToken, err := cookie.GetSession(gc) + if err != nil { + gc.HTML(http.StatusOK, template, gin.H{ + "target_origin": nil, + "authorization_response": map[string]interface{}{ + "type": "authorization_response", + "response": map[string]string{ + "error": "login_required", + "error_description": "Login is required", + }, + }, + }) + return + } + + // get session from cookie + claims, err := token.ValidateBrowserSession(gc, sessionToken) + if err != nil { + gc.HTML(http.StatusOK, template, gin.H{ + "target_origin": nil, + "authorization_response": map[string]interface{}{ + "type": "authorization_response", + "response": map[string]string{ + "error": "login_required", + "error_description": "Login is required", + }, + }, + }) + return + } + userID := claims.Subject + user, err := db.Provider.GetUserByID(userID) + if err != nil { + gc.HTML(http.StatusOK, template, gin.H{ + "target_origin": nil, + "authorization_response": map[string]interface{}{ + "type": "authorization_response", + "response": map[string]string{ + "error": "signup_required", + "error_description": "Sign up required", + }, + }, + }) + return + } + + // if user is logged in + // based on the response type, generate the response + if isResponseTypeCode { + // rollover the session for security + sessionstore.RemoveState(sessionToken) + nonce := uuid.New().String() + newSessionTokenData, newSessionToken, err := token.CreateSessionToken(user, nonce, claims.Roles, claims.Scope) + if err != nil { + gc.HTML(http.StatusOK, template, gin.H{ + "target_origin": nil, + "authorization_response": map[string]interface{}{ + "type": "authorization_response", + "response": map[string]string{ + "error": "login_required", + "error_description": "Login is required", + }, + }, + }) + return + } + + sessionstore.SetState(newSessionToken, newSessionTokenData.Nonce+"@"+user.ID) + cookie.SetSession(gc, newSessionToken) + code := uuid.New().String() + sessionstore.SetState("code_challenge_"+codeChallenge, code) + gc.HTML(http.StatusOK, template, gin.H{ + "target_origin": redirectURI, + "authorization_response": map[string]string{ + "code": code, + "state": state, + }, + }) + return + } + + if isResponseTypeToken { + // rollover the session for security + authToken, err := token.CreateAuthToken(gc, user, claims.Roles, claims.Scope) + if err != nil { + gc.HTML(http.StatusOK, template, gin.H{ + "target_origin": nil, + "authorization_response": map[string]interface{}{ + "type": "authorization_response", + "response": map[string]string{ + "error": "login_required", + "error_description": "Login is required", + }, + }, + }) + return + } + sessionstore.RemoveState(sessionToken) + 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) + 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, + }, + }) + return + } + fmt.Println("=> returning from here...") + + // by default return with error + gc.HTML(http.StatusOK, template, gin.H{ + "target_origin": nil, + "authorization_response": map[string]interface{}{ + "type": "authorization_response", + "response": map[string]string{ + "error": "login_required", + "error_description": "Login is required", + }, + }, + }) } } diff --git a/server/handlers/logout.go b/server/handlers/logout.go new file mode 100644 index 0000000..9ffc6cc --- /dev/null +++ b/server/handlers/logout.go @@ -0,0 +1,40 @@ +package handlers + +import ( + "net/http" + + "github.com/authorizerdev/authorizer/server/cookie" + "github.com/authorizerdev/authorizer/server/crypto" + "github.com/authorizerdev/authorizer/server/sessionstore" + "github.com/gin-gonic/gin" +) + +func LogoutHandler() gin.HandlerFunc { + return func(gc *gin.Context) { + // get fingerprint hash + fingerprintHash, err := cookie.GetSession(gc) + if err != nil { + gc.JSON(http.StatusUnauthorized, gin.H{ + "error": err.Error(), + }) + return + } + + decryptedFingerPrint, err := crypto.DecryptAES(fingerprintHash) + if err != nil { + gc.JSON(http.StatusUnauthorized, gin.H{ + "error": err.Error(), + }) + return + } + + fingerPrint := string(decryptedFingerPrint) + + sessionstore.RemoveState(fingerPrint) + cookie.DeleteSession(gc) + + gc.JSON(http.StatusOK, gin.H{ + "message": "Logged out successfully", + }) + } +} diff --git a/server/handlers/userinfo.go b/server/handlers/userinfo.go new file mode 100644 index 0000000..9e9c6f5 --- /dev/null +++ b/server/handlers/userinfo.go @@ -0,0 +1,40 @@ +package handlers + +import ( + "net/http" + + "github.com/authorizerdev/authorizer/server/db" + "github.com/authorizerdev/authorizer/server/token" + "github.com/gin-gonic/gin" +) + +func UserInfoHandler() gin.HandlerFunc { + return func(gc *gin.Context) { + accessToken, err := token.GetAccessToken(gc) + if err != nil { + gc.JSON(http.StatusUnauthorized, gin.H{ + "error": err.Error(), + }) + return + } + + claims, err := token.ValidateAccessToken(gc, accessToken) + if err != nil { + gc.JSON(http.StatusUnauthorized, gin.H{ + "error": err.Error(), + }) + return + } + + userID := claims["sub"].(string) + user, err := db.Provider.GetUserByID(userID) + if err != nil { + gc.JSON(http.StatusUnauthorized, gin.H{ + "error": err.Error(), + }) + return + } + + gc.JSON(http.StatusOK, user.AsAPIUser()) + } +} diff --git a/server/routes/routes.go b/server/routes/routes.go index 0544431..a8599fc 100644 --- a/server/routes/routes.go +++ b/server/routes/routes.go @@ -23,6 +23,9 @@ func InitRouter() *gin.Engine { // OPEN ID routes router.GET("/.well-known/openid-configuration", handlers.OpenIDConfigurationHandler()) router.GET("/.well-known/jwks.json", handlers.JWKsHandler()) + router.GET("/authorize", handlers.AuthorizeHandler()) + router.GET("/userinfo", handlers.UserInfoHandler()) + router.GET("/logout", handlers.LogoutHandler()) router.LoadHTMLGlob("templates/*") // login page app related routes. diff --git a/server/sessionstore/redis_store.go b/server/sessionstore/redis_store.go index 7e48335..4fad694 100644 --- a/server/sessionstore/redis_store.go +++ b/server/sessionstore/redis_store.go @@ -52,7 +52,7 @@ func (c *RedisStore) DeleteAllUserSession(userId string) { // SetState sets the state in redis store. func (c *RedisStore) SetState(key, value string) { - err := c.store.Set(c.ctx, key, value, 0).Err() + err := c.store.Set(c.ctx, "authorizer_"+key, value, 0).Err() if err != nil { log.Fatalln("Error saving redis token:", err) } @@ -61,7 +61,7 @@ func (c *RedisStore) SetState(key, value string) { // GetState gets the state from redis store. func (c *RedisStore) GetState(key string) string { state := "" - state, err := c.store.Get(c.ctx, key).Result() + state, err := c.store.Get(c.ctx, "authorizer_"+key).Result() if err != nil { log.Println("error getting token from redis store:", err) } @@ -71,7 +71,7 @@ func (c *RedisStore) GetState(key string) string { // RemoveState removes the state from redis store. func (c *RedisStore) RemoveState(key string) { - err := c.store.Del(c.ctx, key).Err() + err := c.store.Del(c.ctx, "authorizer_"+key).Err() if err != nil { log.Fatalln("Error deleting redis token:", err) } diff --git a/templates/authorize.tmpl b/templates/authorize.tmpl index a42c15b..8842463 100644 --- a/templates/authorize.tmpl +++ b/templates/authorize.tmpl @@ -6,8 +6,8 @@