package handlers /** LOGIC TO REMEMBER THE AUTHORIZE FLOW jargons `at_hash` -> access_token_hash `c_hash` -> code_hash # ResponseType: Code with /authorize request - set state [state, code@@challenge] - add &code to login redirect url login resolver has optional param state -if state found in store, split with @@ - if len > 1 -> response type is code and has code + challenge - set `nonce@@code` for createAuthToken request so that `c_hash` can be generated - do not add `nonce` to id_token in code flow, instead set `c_hash` and `at_hash` # ResponseType: token / id_token with /authorize request - set state [state, nonce] - add &nonce to login redirect url login resolver has optional param state - if state found in store, split with @@ - if len < 1 -> response type is token / id_token and has nonce - send received nonce for createAuthToken - set `nonce` and `at_hash` in `id_token` **/ import ( "fmt" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/authorizerdev/authorizer/server/constants" "github.com/authorizerdev/authorizer/server/cookie" "github.com/authorizerdev/authorizer/server/db" "github.com/authorizerdev/authorizer/server/memorystore" "github.com/authorizerdev/authorizer/server/parsers" "github.com/authorizerdev/authorizer/server/token" "github.com/authorizerdev/authorizer/server/utils" ) // 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. // Check following docs for understanding request / response params for various types of requests: https://auth0.com/docs/authenticate/login/oidc-conformant-authentication/oidc-adoption-auth-code-flow const ( authorizeWebMessageTemplate = "authorize_web_message.tmpl" authorizeFormPostTemplate = "authorize_form_post.tmpl" ) // AuthorizeHandler is the handler for the /authorize route // required params // ?redirect_uri = redirect url // ?response_mode = to decide if result should be html or re-direct // state[recommended] = to prevent CSRF attack (for authorizer its compulsory) // code_challenge = to prevent CSRF attack // code_challenge_method = to prevent CSRF attack [only sh256 is supported] func AuthorizeHandler() gin.HandlerFunc { 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")) scopeString := strings.TrimSpace(gc.Query("scope")) clientID := strings.TrimSpace(gc.Query("client_id")) responseMode := strings.TrimSpace(gc.Query("response_mode")) nonce := strings.TrimSpace(gc.Query("nonce")) var scope []string if scopeString == "" { scope = []string{"openid", "profile", "email"} } else { scope = strings.Split(scopeString, " ") } if responseMode == "" { responseMode = constants.ResponseModeQuery } if redirectURI == "" { redirectURI = "/app" } if responseType == "" { responseType = "token" } if err := validateAuthorizeRequest(responseType, responseMode, clientID, state, codeChallenge); err != nil { log.Debug("invalid authorization request: ", err) gc.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } code := uuid.New().String() if nonce == "" { nonce = uuid.New().String() } log := log.WithFields(log.Fields{ "response_mode": responseMode, "response_type": responseType, "state": state, "code_challenge": codeChallenge, "scope": scope, "redirect_uri": redirectURI, "nonce": nonce, "code": code, }) // memorystore.Provider.SetState(codeChallenge, code) // TODO add state with timeout // used for response mode query or fragment loginState := "state=" + state + "&scope=" + strings.Join(scope, " ") + "&redirect_uri=" + redirectURI if responseType == constants.ResponseTypeCode { loginState += "&code=" + code if err := memorystore.Provider.SetState(state, code+"@@"+codeChallenge); err != nil { log.Debug("Error setting temp code", err) } } else { loginState += "&nonce=" + nonce if err := memorystore.Provider.SetState(state, nonce); err != nil { log.Debug("Error setting temp code", err) } } loginURL := "/app?" + loginState if responseMode == constants.ResponseModeFragment { loginURL = "/app#" + loginState } if state == "" { handleResponse(gc, responseMode, loginURL, redirectURI, map[string]interface{}{ "type": "authorization_response", "response": map[string]interface{}{ "error": "state_required", "error_description": "state is required", }, }, http.StatusOK) return } if responseType == constants.ResponseTypeCode && codeChallenge == "" { handleResponse(gc, responseMode, loginURL, redirectURI, map[string]interface{}{ "type": "authorization_response", "response": map[string]interface{}{ "error": "code_challenge_required", "error_description": "code challenge is required", }, }, http.StatusOK) } loginError := map[string]interface{}{ "type": "authorization_response", "response": map[string]interface{}{ "error": "login_required", "error_description": "Login is required", }, } sessionToken, err := cookie.GetSession(gc) if err != nil { log.Debug("GetSession failed: ", err) handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) return } // get session from cookie claims, err := token.ValidateBrowserSession(gc, sessionToken) if err != nil { log.Debug("ValidateBrowserSession failed: ", err) handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) return } userID := claims.Subject user, err := db.Provider.GetUserByID(gc, userID) if err != nil { log.Debug("GetUserByID failed: ", err) handleResponse(gc, responseMode, loginURL, redirectURI, map[string]interface{}{ "type": "authorization_response", "response": map[string]interface{}{ "error": "signup_required", "error_description": "Sign up required", }, }, http.StatusOK) return } sessionKey := user.ID if claims.LoginMethod != "" { sessionKey = claims.LoginMethod + ":" + user.ID } // rollover the session for security go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce) if responseType == constants.ResponseTypeCode { newSessionTokenData, newSessionToken, err := token.CreateSessionToken(user, nonce, claims.Roles, scope, claims.LoginMethod) if err != nil { log.Debug("CreateSessionToken failed: ", err) handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) return } // TODO: add state with timeout // if err := memorystore.Provider.SetState(codeChallenge, code+"@"+newSessionToken); err != nil { // log.Debug("SetState failed: ", err) // handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) // return // } // TODO: add state with timeout if err := memorystore.Provider.SetState(code, codeChallenge+"@@"+newSessionToken); err != nil { log.Debug("SetState failed: ", err) handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) return } if err := memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+newSessionTokenData.Nonce, newSessionToken); err != nil { log.Debug("SetUserSession failed: ", err) handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) return } cookie.SetSession(gc, newSessionToken) // in case, response type is code and user is already logged in send the code and state // and cookie session will already be rolled over and set // gc.HTML(http.StatusOK, authorizeWebMessageTemplate, gin.H{ // "target_origin": redirectURI, // "authorization_response": map[string]interface{}{ // "type": "authorization_response", // "response": map[string]string{ // "code": code, // "state": state, // }, // }, // }) params := "code=" + code + "&state=" + state + "&nonce=" + nonce if responseMode == constants.ResponseModeQuery { if strings.Contains(redirectURI, "?") { redirectURI = redirectURI + "&" + params } else { redirectURI = redirectURI + "?" + params } } else if responseMode == constants.ResponseModeFragment { if strings.Contains(redirectURI, "#") { redirectURI = redirectURI + "&" + params } else { redirectURI = redirectURI + "#" + params } } handleResponse(gc, responseMode, loginURL, redirectURI, map[string]interface{}{ "type": "authorization_response", "response": map[string]interface{}{ "code": code, "state": state, }, }, http.StatusOK) return } if responseType == constants.ResponseTypeToken || responseType == constants.ResponseTypeIDToken { hostname := parsers.GetHost(gc) // rollover the session for security authToken, err := token.CreateAuthToken(gc, user, claims.Roles, scope, claims.LoginMethod, nonce) if err != nil { log.Debug("CreateAuthToken failed: ", err) handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) return } if err := memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+nonce, authToken.FingerPrintHash); err != nil { log.Debug("SetUserSession failed: ", err) handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) return } if err := memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+nonce, authToken.FingerPrintHash); err != nil { log.Debug("SetUserSession failed: ", err) handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) return } cookie.SetSession(gc, authToken.FingerPrintHash) // used of query mode params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(authToken.IDToken.ExpiresAt, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token + "&code=" + code res := map[string]interface{}{ "access_token": authToken.AccessToken.Token, "id_token": authToken.IDToken.Token, "state": state, "scope": scope, "token_type": "Bearer", "expires_in": authToken.AccessToken.ExpiresAt, "code": code, } if utils.StringSliceContains(scope, "offline_access") { refreshToken, _, err := token.CreateRefreshToken(user, claims.Roles, scope, hostname, nonce, claims.LoginMethod) if err != nil { log.Debug("SetUserSession failed: ", err) handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) return } res["refresh_token"] = refreshToken params += "&refresh_token=" + refreshToken memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeRefreshToken+"_"+nonce, refreshToken) } if responseMode == constants.ResponseModeQuery { if strings.Contains(redirectURI, "?") { redirectURI = redirectURI + "&" + params } else { redirectURI = redirectURI + "?" + params } } else if responseMode == constants.ResponseModeFragment { if strings.Contains(redirectURI, "#") { redirectURI = redirectURI + "&" + params } else { redirectURI = redirectURI + "#" + params } } handleResponse(gc, responseMode, loginURL, redirectURI, map[string]interface{}{ "type": "authorization_response", "response": res, }, http.StatusOK) return } handleResponse(gc, responseMode, loginURL, redirectURI, loginError, http.StatusOK) } } func validateAuthorizeRequest(responseType, responseMode, clientID, state, codeChallenge string) error { if responseType != constants.ResponseTypeCode && responseType != constants.ResponseTypeToken && responseType != constants.ResponseTypeIDToken { return fmt.Errorf("invalid response type %s. 'code' & 'token' are valid response_type", responseMode) } if responseMode != constants.ResponseModeQuery && responseMode != constants.ResponseModeWebMessage && responseMode != constants.ResponseModeFragment && responseMode != constants.ResponseModeFormPost { return fmt.Errorf("invalid response mode %s. 'query', 'fragment', 'form_post' and 'web_message' are valid response_mode", responseMode) } if client, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyClientID); client != clientID || err != nil { return fmt.Errorf("invalid client_id %s", clientID) } return nil } func handleResponse(gc *gin.Context, responseMode, loginURI, redirectURI string, data map[string]interface{}, httpStatusCode int) { isAuthenticationRequired := false if _, ok := data["response"].(map[string]interface{})["error"]; ok { isAuthenticationRequired = true } if isAuthenticationRequired { gc.Redirect(http.StatusFound, loginURI) return } switch responseMode { case constants.ResponseModeQuery, constants.ResponseModeFragment: gc.Redirect(http.StatusFound, redirectURI) return case constants.ResponseModeWebMessage: gc.HTML(httpStatusCode, authorizeWebMessageTemplate, gin.H{ "target_origin": redirectURI, "authorization_response": data, }) return case constants.ResponseModeFormPost: fmt.Println("=> trying tof orm post") fmt.Printf("=> %+v \n", data["response"]) gc.HTML(httpStatusCode, authorizeFormPostTemplate, gin.H{ "target_origin": redirectURI, "authorization_response": data["response"], }) return } }