2022-02-28 15:56:49 +00:00
package handlers
2022-11-12 19:10:28 +00:00
/ * *
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
2022-11-12 19:52:21 +00:00
- set ` nonce, code ` for createAuthToken request so that ` c_hash ` can be generated
2022-11-12 19:10:28 +00:00
- 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 @ @
2022-11-12 19:52:21 +00:00
- if len < 1 - > response type is token / id_token and value is nonce
- send received nonce for createAuthToken with empty code value
2022-11-12 19:10:28 +00:00
- set ` nonce ` and ` at_hash ` in ` id_token `
* * /
2022-02-28 15:56:49 +00:00
import (
2022-10-09 14:18:13 +00:00
"fmt"
2022-02-28 15:56:49 +00:00
"net/http"
2022-03-08 07:06:26 +00:00
"strconv"
2022-02-28 15:56:49 +00:00
"strings"
2022-05-23 06:22:51 +00:00
"github.com/gin-gonic/gin"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
2022-03-07 03:01:39 +00:00
"github.com/authorizerdev/authorizer/server/constants"
2022-03-03 19:06:27 +00:00
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db"
2022-05-27 17:50:38 +00:00
"github.com/authorizerdev/authorizer/server/memorystore"
2022-03-03 19:06:27 +00:00
"github.com/authorizerdev/authorizer/server/token"
2022-02-28 15:56:49 +00:00
)
2022-11-12 19:10:28 +00:00
// 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.
2022-03-03 19:06:27 +00:00
2022-11-12 19:10:28 +00:00
// 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
2022-10-16 15:16:54 +00:00
const (
authorizeWebMessageTemplate = "authorize_web_message.tmpl"
authorizeFormPostTemplate = "authorize_form_post.tmpl"
)
2022-11-12 19:10:28 +00:00
// 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]
2022-02-28 15:56:49 +00:00
func AuthorizeHandler ( ) gin . HandlerFunc {
2022-03-03 19:06:27 +00:00
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" ) )
2022-03-04 07:26:11 +00:00
scopeString := strings . TrimSpace ( gc . Query ( "scope" ) )
2022-03-07 03:01:39 +00:00
clientID := strings . TrimSpace ( gc . Query ( "client_id" ) )
2022-03-07 13:19:18 +00:00
responseMode := strings . TrimSpace ( gc . Query ( "response_mode" ) )
2022-10-19 18:06:33 +00:00
nonce := strings . TrimSpace ( gc . Query ( "nonce" ) )
2022-02-28 15:56:49 +00:00
2022-03-08 07:06:26 +00:00
var scope [ ] string
if scopeString == "" {
scope = [ ] string { "openid" , "profile" , "email" }
} else {
scope = strings . Split ( scopeString , " " )
}
2022-03-07 13:19:18 +00:00
if responseMode == "" {
2023-04-01 12:06:07 +00:00
if val , err := memorystore . Provider . GetStringStoreEnvVariable ( constants . EnvKeyDefaultAuthorizeResponseMode ) ; err == nil {
2023-06-29 15:10:44 +00:00
responseMode = val
2023-04-01 12:06:07 +00:00
} else {
2023-06-29 15:10:44 +00:00
responseMode = constants . ResponseModeQuery
2023-04-01 12:06:07 +00:00
}
2022-03-07 13:19:18 +00:00
}
if redirectURI == "" {
redirectURI = "/app"
}
2022-02-28 15:56:49 +00:00
if responseType == "" {
2023-04-01 12:06:07 +00:00
if val , err := memorystore . Provider . GetStringStoreEnvVariable ( constants . EnvKeyDefaultAuthorizeResponseType ) ; err == nil {
responseType = val
} else {
responseType = constants . ResponseTypeToken
}
2022-02-28 15:56:49 +00:00
}
2022-10-09 14:18:13 +00:00
if err := validateAuthorizeRequest ( responseType , responseMode , clientID , state , codeChallenge ) ; err != nil {
log . Debug ( "invalid authorization request: " , err )
2022-10-18 17:44:24 +00:00
gc . JSON ( http . StatusBadRequest , gin . H { "error" : err . Error ( ) } )
2022-03-07 13:19:18 +00:00
return
}
2022-10-20 10:05:26 +00:00
code := uuid . New ( ) . String ( )
if nonce == "" {
nonce = uuid . New ( ) . String ( )
}
2022-10-18 15:38:53 +00:00
log := log . WithFields ( log . Fields {
2022-11-15 16:15:08 +00:00
"response_mode" : responseMode ,
"response_type" : responseType ,
2022-10-18 15:38:53 +00:00
} )
2022-11-12 18:24:37 +00:00
// TODO add state with timeout
2022-10-16 15:16:54 +00:00
// used for response mode query or fragment
2022-11-12 18:24:37 +00:00
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 )
}
}
2022-10-16 15:16:54 +00:00
loginURL := "/app?" + loginState
2022-10-18 17:54:19 +00:00
2022-10-16 15:16:54 +00:00
if responseMode == constants . ResponseModeFragment {
loginURL = "/app#" + loginState
}
2022-10-18 17:54:19 +00:00
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 )
2022-11-28 23:57:29 +00:00
return
2022-10-18 17:54:19 +00:00
}
2022-10-16 15:16:54 +00:00
loginError := map [ string ] interface { } {
2022-10-18 15:38:53 +00:00
"type" : "authorization_response" ,
2022-10-18 17:33:52 +00:00
"response" : map [ string ] interface { } {
2022-10-18 15:38:53 +00:00
"error" : "login_required" ,
"error_description" : "Login is required" ,
} ,
2022-10-16 15:16:54 +00:00
}
2022-03-03 19:06:27 +00:00
sessionToken , err := cookie . GetSession ( gc )
if err != nil {
2022-10-09 14:18:13 +00:00
log . Debug ( "GetSession failed: " , err )
2022-10-16 15:16:54 +00:00
handleResponse ( gc , responseMode , loginURL , redirectURI , loginError , http . StatusOK )
2022-03-03 19:06:27 +00:00
return
}
// get session from cookie
claims , err := token . ValidateBrowserSession ( gc , sessionToken )
if err != nil {
2022-10-09 14:18:13 +00:00
log . Debug ( "ValidateBrowserSession failed: " , err )
2022-10-16 15:16:54 +00:00
handleResponse ( gc , responseMode , loginURL , redirectURI , loginError , http . StatusOK )
2022-03-03 19:06:27 +00:00
return
}
2022-10-18 17:04:57 +00:00
2022-03-03 19:06:27 +00:00
userID := claims . Subject
2022-07-10 16:19:33 +00:00
user , err := db . Provider . GetUserByID ( gc , userID )
2022-03-03 19:06:27 +00:00
if err != nil {
2022-10-09 14:18:13 +00:00
log . Debug ( "GetUserByID failed: " , err )
2022-10-16 15:16:54 +00:00
handleResponse ( gc , responseMode , loginURL , redirectURI , map [ string ] interface { } {
2022-10-18 15:38:53 +00:00
"type" : "authorization_response" ,
2022-10-18 17:33:52 +00:00
"response" : map [ string ] interface { } {
2022-10-18 15:38:53 +00:00
"error" : "signup_required" ,
"error_description" : "Sign up required" ,
} ,
2022-10-16 15:16:54 +00:00
} , http . StatusOK )
2022-03-03 19:06:27 +00:00
return
}
2022-06-29 16:54:00 +00:00
sessionKey := user . ID
if claims . LoginMethod != "" {
sessionKey = claims . LoginMethod + ":" + user . ID
}
2022-10-12 07:40:24 +00:00
// rollover the session for security
go memorystore . Provider . DeleteUserSession ( sessionKey , claims . Nonce )
2022-10-16 15:16:54 +00:00
if responseType == constants . ResponseTypeCode {
2023-04-08 07:36:15 +00:00
newSessionTokenData , newSessionToken , newSessionExpiresAt , err := token . CreateSessionToken ( user , nonce , claims . Roles , scope , claims . LoginMethod )
2022-10-20 10:05:26 +00:00
if err != nil {
log . Debug ( "CreateSessionToken failed: " , err )
handleResponse ( gc , responseMode , loginURL , redirectURI , loginError , http . StatusOK )
return
}
2022-11-12 18:24:37 +00:00
// 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 {
2022-10-20 10:05:26 +00:00
log . Debug ( "SetState failed: " , err )
handleResponse ( gc , responseMode , loginURL , redirectURI , loginError , http . StatusOK )
return
}
2023-04-08 07:36:15 +00:00
if err := memorystore . Provider . SetUserSession ( sessionKey , constants . TokenTypeSessionToken + "_" + newSessionTokenData . Nonce , newSessionToken , newSessionExpiresAt ) ; err != nil {
2022-10-16 15:16:54 +00:00
log . Debug ( "SetUserSession failed: " , err )
handleResponse ( gc , responseMode , loginURL , redirectURI , loginError , http . StatusOK )
2022-03-03 19:06:27 +00:00
return
}
cookie . SetSession ( gc , newSessionToken )
2022-10-16 15:16:54 +00:00
// 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
2022-10-18 15:59:09 +00:00
// 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,
// },
// },
// })
2022-10-18 15:38:53 +00:00
2022-10-19 18:44:06 +00:00
params := "code=" + code + "&state=" + state + "&nonce=" + nonce
2022-10-18 16:30:54 +00:00
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
}
}
2022-10-16 16:46:37 +00:00
2022-10-18 15:59:09 +00:00
handleResponse ( gc , responseMode , loginURL , redirectURI , map [ string ] interface { } {
2022-10-18 16:16:37 +00:00
"type" : "authorization_response" ,
2022-10-18 17:33:52 +00:00
"response" : map [ string ] interface { } {
2022-10-18 16:16:37 +00:00
"code" : code ,
"state" : state ,
} ,
2022-10-18 15:59:09 +00:00
} , http . StatusOK )
2022-10-16 16:46:37 +00:00
2022-03-03 19:06:27 +00:00
return
}
2022-10-16 15:33:37 +00:00
if responseType == constants . ResponseTypeToken || responseType == constants . ResponseTypeIDToken {
2022-10-23 15:38:08 +00:00
// rollover the session for security
2022-11-12 19:52:21 +00:00
authToken , err := token . CreateAuthToken ( gc , user , claims . Roles , scope , claims . LoginMethod , nonce , "" )
2022-10-20 10:05:26 +00:00
if err != nil {
2022-10-23 15:38:08 +00:00
log . Debug ( "CreateAuthToken failed: " , err )
2022-10-16 15:16:54 +00:00
handleResponse ( gc , responseMode , loginURL , redirectURI , loginError , http . StatusOK )
return
}
2023-04-08 07:36:15 +00:00
if err := memorystore . Provider . SetUserSession ( sessionKey , constants . TokenTypeSessionToken + "_" + nonce , authToken . FingerPrintHash , authToken . SessionTokenExpiresAt ) ; err != nil {
2022-10-16 15:16:54 +00:00
log . Debug ( "SetUserSession failed: " , err )
handleResponse ( gc , responseMode , loginURL , redirectURI , loginError , http . StatusOK )
return
}
2023-04-08 07:36:15 +00:00
if err := memorystore . Provider . SetUserSession ( sessionKey , constants . TokenTypeAccessToken + "_" + nonce , authToken . AccessToken . Token , authToken . AccessToken . ExpiresAt ) ; err != nil {
2022-10-16 15:16:54 +00:00
log . Debug ( "SetUserSession failed: " , err )
handleResponse ( gc , responseMode , loginURL , redirectURI , loginError , http . StatusOK )
2022-02-28 15:56:49 +00:00
return
}
2022-06-29 16:54:00 +00:00
2022-10-23 15:38:08 +00:00
cookie . SetSession ( gc , authToken . FingerPrintHash )
2022-03-08 07:06:26 +00:00
// used of query mode
2022-11-15 16:15:08 +00:00
params := "access_token=" + authToken . AccessToken . Token + "&token_type=bearer&expires_in=" + strconv . FormatInt ( authToken . IDToken . ExpiresAt , 10 ) + "&state=" + state + "&id_token=" + authToken . IDToken . Token
2022-03-08 07:06:26 +00:00
2022-03-04 07:26:11 +00:00
res := map [ string ] interface { } {
2022-10-23 15:38:08 +00:00
"access_token" : authToken . AccessToken . Token ,
"id_token" : authToken . IDToken . Token ,
2022-03-04 07:26:11 +00:00
"state" : state ,
2023-02-08 04:09:08 +00:00
"scope" : strings . Join ( scope , " " ) ,
2022-03-04 07:26:11 +00:00
"token_type" : "Bearer" ,
2022-10-23 15:38:08 +00:00
"expires_in" : authToken . AccessToken . ExpiresAt ,
2022-03-04 07:26:11 +00:00
}
2022-11-15 16:15:08 +00:00
if nonce != "" {
params += "&nonce=" + nonce
res [ "nonce" ] = nonce
}
if authToken . RefreshToken != nil {
res [ "refresh_token" ] = authToken . RefreshToken . Token
params += "&refresh_token=" + authToken . RefreshToken . Token
2023-04-08 07:36:15 +00:00
memorystore . Provider . SetUserSession ( sessionKey , constants . TokenTypeRefreshToken + "_" + authToken . FingerPrint , authToken . RefreshToken . Token , authToken . RefreshToken . ExpiresAt )
2022-03-04 07:26:11 +00:00
}
2022-10-16 15:16:54 +00:00
if responseMode == constants . ResponseModeQuery {
2022-03-08 07:06:26 +00:00
if strings . Contains ( redirectURI , "?" ) {
2022-10-16 15:16:54 +00:00
redirectURI = redirectURI + "&" + params
2022-03-08 07:06:26 +00:00
} else {
2022-10-16 15:16:54 +00:00
redirectURI = redirectURI + "?" + params
}
} else if responseMode == constants . ResponseModeFragment {
if strings . Contains ( redirectURI , "#" ) {
redirectURI = redirectURI + "&" + params
} else {
redirectURI = redirectURI + "#" + params
2022-03-08 07:06:26 +00:00
}
}
2022-10-16 15:16:54 +00:00
handleResponse ( gc , responseMode , loginURL , redirectURI , map [ string ] interface { } {
"type" : "authorization_response" ,
"response" : res ,
} , http . StatusOK )
2022-03-03 19:06:27 +00:00
return
2022-02-28 15:56:49 +00:00
}
2022-03-03 19:06:27 +00:00
2022-10-16 15:16:54 +00:00
handleResponse ( gc , responseMode , loginURL , redirectURI , loginError , http . StatusOK )
2022-02-28 15:56:49 +00:00
}
}
2022-10-09 14:18:13 +00:00
func validateAuthorizeRequest ( responseType , responseMode , clientID , state , codeChallenge string ) error {
2022-11-15 16:15:08 +00:00
if strings . TrimSpace ( state ) == "" {
2022-12-23 17:16:08 +00:00
return fmt . Errorf ( "invalid state. state is required to prevent csrf attack" )
2022-11-15 16:15:08 +00:00
}
2022-10-16 15:33:37 +00:00
if responseType != constants . ResponseTypeCode && responseType != constants . ResponseTypeToken && responseType != constants . ResponseTypeIDToken {
2022-10-09 14:18:13 +00:00
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 {
2022-10-16 15:16:54 +00:00
return fmt . Errorf ( "invalid response mode %s. 'query', 'fragment', 'form_post' and 'web_message' are valid response_mode" , responseMode )
2022-10-09 14:18:13 +00:00
}
if client , err := memorystore . Provider . GetStringStoreEnvVariable ( constants . EnvKeyClientID ) ; client != clientID || err != nil {
return fmt . Errorf ( "invalid client_id %s" , clientID )
}
return nil
}
2022-10-16 15:16:54 +00:00
func handleResponse ( gc * gin . Context , responseMode , loginURI , redirectURI string , data map [ string ] interface { } , httpStatusCode int ) {
isAuthenticationRequired := false
2022-10-18 17:33:52 +00:00
if _ , ok := data [ "response" ] . ( map [ string ] interface { } ) [ "error" ] ; ok {
2022-10-18 15:38:53 +00:00
isAuthenticationRequired = true
2022-10-16 15:16:54 +00:00
}
2022-11-28 23:57:29 +00:00
if isAuthenticationRequired && responseMode != constants . ResponseModeWebMessage {
2022-11-12 18:24:37 +00:00
gc . Redirect ( http . StatusFound , loginURI )
return
}
2022-10-16 15:16:54 +00:00
switch responseMode {
case constants . ResponseModeQuery , constants . ResponseModeFragment :
2022-11-12 18:24:37 +00:00
gc . Redirect ( http . StatusFound , redirectURI )
2022-10-16 15:16:54 +00:00
return
case constants . ResponseModeWebMessage :
gc . HTML ( httpStatusCode , authorizeWebMessageTemplate , gin . H {
"target_origin" : redirectURI ,
"authorization_response" : data ,
} )
return
case constants . ResponseModeFormPost :
gc . HTML ( httpStatusCode , authorizeFormPostTemplate , gin . H {
"target_origin" : redirectURI ,
2022-10-18 17:33:52 +00:00
"authorization_response" : data [ "response" ] ,
2022-10-16 15:16:54 +00:00
} )
return
}
}