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