diff --git a/server/constants/oauth2.go b/server/constants/oauth2.go new file mode 100644 index 0000000..d3ff253 --- /dev/null +++ b/server/constants/oauth2.go @@ -0,0 +1,17 @@ +package constants + +const ( + // - query: for Authorization Code grant. 302 Found triggers redirect. + ResponseModeQuery = "query" + // - fragment: for Implicit grant. 302 Found triggers redirect. + ResponseModeFragment = "fragment" + // - form_post: 200 OK with response parameters embedded in an HTML form as hidden parameters. + ResponseModeFormPost = "form_post" + // - web_message: For Silent Authentication. Uses HTML5 web messaging. + ResponseModeWebMessage = "web_message" + + // For the Authorization Code grant, use response_type=code to include the authorization code. + ResponseTypeCode = "code" + // For the Implicit grant, use response_type=token to include an access token. + ResponseTypeToken = "token" +) diff --git a/server/handlers/authorize.go b/server/handlers/authorize.go index fd2372c..b3db022 100644 --- a/server/handlers/authorize.go +++ b/server/handlers/authorize.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "net/http" "strconv" "strings" @@ -45,176 +46,42 @@ func AuthorizeHandler() gin.HandlerFunc { } if responseMode == "" { - responseMode = "query" - } - - if responseMode != "query" && responseMode != "web_message" { - log.Debug("Invalid response_mode: ", responseMode) - gc.JSON(400, gin.H{"error": "invalid response mode"}) + responseMode = constants.ResponseModeQuery } if redirectURI == "" { redirectURI = "/app" } - isQuery := responseMode == "query" - - loginURL := "/app?state=" + state + "&scope=" + strings.Join(scope, " ") + "&redirect_uri=" + redirectURI - - if clientID == "" { - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } else { - log.Debug("Failed to get client_id: ", clientID) - gc.HTML(http.StatusOK, template, gin.H{ - "target_origin": redirectURI, - "authorization_response": map[string]interface{}{ - "type": "authorization_response", - "response": map[string]string{ - "error": "client_id is required", - }, - }, - }) - } - return - } - - if client, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyClientID); client != clientID || err != nil { - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } else { - log.Debug("Invalid client_id: ", clientID) - gc.HTML(http.StatusOK, template, gin.H{ - "target_origin": redirectURI, - "authorization_response": map[string]interface{}{ - "type": "authorization_response", - "response": map[string]string{ - "error": "invalid_client_id", - }, - }, - }) - } - return - } - - if state == "" { - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } else { - log.Debug("Failed to get state: ", state) - gc.HTML(http.StatusOK, template, gin.H{ - "target_origin": redirectURI, - "authorization_response": map[string]interface{}{ - "type": "authorization_response", - "response": map[string]string{ - "error": "state is required", - }, - }, - }) - } - return - } - if responseType == "" { responseType = "token" } - isResponseTypeCode := responseType == "code" - isResponseTypeToken := responseType == "token" - - if !isResponseTypeCode && !isResponseTypeToken { - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } else { - log.Debug("Invalid response_type: ", responseType) - gc.HTML(http.StatusOK, template, gin.H{ - "target_origin": redirectURI, - "authorization_response": map[string]interface{}{ - "type": "authorization_response", - "response": map[string]string{ - "error": "response_type is invalid", - }, - }, - }) - } + if err := validateAuthorizeRequest(responseType, responseMode, clientID, state, codeChallenge); err != nil { + log.Debug("invalid authorization request: ", err) + gc.JSON(http.StatusBadRequest, gin.H{"error": err}) return } - if isResponseTypeCode { - if codeChallenge == "" { - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } else { - log.Debug("Failed to get code_challenge: ", codeChallenge) - gc.HTML(http.StatusBadRequest, template, gin.H{ - "target_origin": redirectURI, - "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 { - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } else { - gc.HTML(http.StatusOK, template, gin.H{ - "target_origin": redirectURI, - "authorization_response": map[string]interface{}{ - "type": "authorization_response", - "response": map[string]string{ - "error": "login_required", - "error_description": "Login is required", - }, - }, - }) - } + log.Debug("GetSession failed: ", err) + gc.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("login required. %v", err)}) return } // get session from cookie claims, err := token.ValidateBrowserSession(gc, sessionToken) if err != nil { - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } else { - gc.HTML(http.StatusOK, template, gin.H{ - "target_origin": redirectURI, - "authorization_response": map[string]interface{}{ - "type": "authorization_response", - "response": map[string]string{ - "error": "login_required", - "error_description": "Login is required", - }, - }, - }) - } + log.Debug("ValidateBrowserSession failed: ", err) + gc.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("login required. %v", err)}) return } userID := claims.Subject user, err := db.Provider.GetUserByID(gc, userID) if err != nil { - if isQuery { - gc.Redirect(http.StatusFound, loginURL) - } else { - gc.HTML(http.StatusOK, template, gin.H{ - "target_origin": redirectURI, - "authorization_response": map[string]interface{}{ - "type": "authorization_response", - "response": map[string]string{ - "error": "signup_required", - "error_description": "Sign up required", - }, - }, - }) - } + log.Debug("GetUserByID failed: ", err) + gc.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("sign up required. %v", err)}) return } @@ -223,6 +90,12 @@ func AuthorizeHandler() gin.HandlerFunc { sessionKey = claims.LoginMethod + ":" + user.ID } + loginState := "state=" + state + "&scope=" + strings.Join(scope, " ") + "&redirect_uri=" + redirectURI + loginURL := "/app?" + loginState + if responseMode == constants.ResponseModeFragment { + loginURL = "/app#" + loginState + } + // if user is logged in // based on the response type code, generate the response if isResponseTypeCode { @@ -349,3 +222,27 @@ func AuthorizeHandler() gin.HandlerFunc { } } } + +func validateAuthorizeRequest(responseType, responseMode, clientID, state, codeChallenge string) error { + if responseType != constants.ResponseTypeCode && responseType != constants.ResponseTypeToken { + 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") + } + + if responseType == constants.ResponseTypeCode && strings.TrimSpace(codeChallenge) == "" { + return fmt.Errorf("code_challenge is required for %s '%s'", responseType, constants.ResponseTypeCode) + } + + if client, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyClientID); client != clientID || err != nil { + return fmt.Errorf("invalid client_id %s", clientID) + } + + if strings.TrimSpace(state) == "" { + return fmt.Errorf("state is required") + } + + return nil +} diff --git a/server/memorystore/memory_store.go b/server/memorystore/memory_store.go index c112c01..15b7248 100644 --- a/server/memorystore/memory_store.go +++ b/server/memorystore/memory_store.go @@ -57,7 +57,7 @@ func InitMemStore() error { } redisURL := requiredEnvs.RedisURL - if redisURL != "" && !requiredEnvs.disableRedisForEnv { + if redisURL != "" && !requiredEnvs.DisableRedisForEnv { log.Info("Initializing Redis memory store") Provider, err = redis.NewRedisProvider(redisURL) if err != nil { diff --git a/server/memorystore/required_env_store.go b/server/memorystore/required_env_store.go index a5f3a81..b81cf56 100644 --- a/server/memorystore/required_env_store.go +++ b/server/memorystore/required_env_store.go @@ -27,7 +27,7 @@ type RequiredEnv struct { DatabaseCertKey string `json:"DATABASE_CERT_KEY"` DatabaseCACert string `json:"DATABASE_CA_CERT"` RedisURL string `json:"REDIS_URL"` - disableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"` + DisableRedisForEnv bool `json:"DISABLE_REDIS_FOR_ENV"` } // RequiredEnvObj is a simple in-memory store for sessions. @@ -138,7 +138,7 @@ func InitRequiredEnv() error { DatabaseCertKey: dbCertKey, DatabaseCACert: dbCACert, RedisURL: redisURL, - disableRedisForEnv: disableRedisForEnv, + DisableRedisForEnv: disableRedisForEnv, } RequiredEnvStoreObj = &RequiredEnvStore{