2022-02-28 15:56:49 +00:00
package handlers
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-03-25 12:21:20 +00:00
"time"
2022-02-28 15:56:49 +00:00
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
)
// AuthorizeHandler is the handler for the /authorize route
// required params
// ?redirect_uri = redirect url
2022-03-07 13:19:18 +00:00
// ?response_mode = to decide if result should be html or re-direct
2022-02-28 15:56:49 +00:00
// 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-03-03 19:06:27 +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-10-16 15:16:54 +00:00
const (
authorizeWebMessageTemplate = "authorize_web_message.tmpl"
authorizeFormPostTemplate = "authorize_form_post.tmpl"
)
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-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 == "" {
2022-10-09 14:18:13 +00:00
responseMode = constants . ResponseModeQuery
2022-03-07 13:19:18 +00:00
}
if redirectURI == "" {
redirectURI = "/app"
}
2022-02-28 15:56:49 +00:00
if responseType == "" {
2022-03-03 19:06:27 +00:00
responseType = "token"
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 )
gc . JSON ( http . StatusBadRequest , gin . H { "error" : err } )
2022-03-07 13:19:18 +00:00
return
}
2022-10-16 15:16:54 +00:00
// used for response mode query or fragment
loginState := "state=" + state + "&scope=" + strings . Join ( scope , " " ) + "&redirect_uri=" + redirectURI
loginURL := "/app?" + loginState
if responseMode == constants . ResponseModeFragment {
loginURL = "/app#" + loginState
}
loginError := map [ string ] interface { } {
"error" : "login_required" ,
"error_description" : "Login is required" ,
}
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
}
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 { } {
"error" : "signup_required" ,
"error_description" : "Sign up required" ,
} , 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 {
2022-03-03 19:06:27 +00:00
nonce := uuid . New ( ) . String ( )
2022-06-29 16:54:00 +00:00
newSessionTokenData , newSessionToken , err := token . CreateSessionToken ( user , nonce , claims . Roles , scope , claims . LoginMethod )
2022-03-03 19:06:27 +00:00
if err != nil {
2022-10-16 15:16:54 +00:00
log . Debug ( "CreateSessionToken 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 )
2022-03-03 19:06:27 +00:00
return
}
cookie . SetSession ( gc , newSessionToken )
code := uuid . New ( ) . String ( )
2022-10-16 15:16:54 +00:00
if err := memorystore . Provider . SetState ( codeChallenge , code + "@" + newSessionToken ) ; err != nil {
log . Debug ( "SetState failed: " , err )
handleResponse ( gc , responseMode , loginURL , redirectURI , loginError , http . StatusOK )
return
}
// 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 {
2022-03-03 19:06:27 +00:00
"target_origin" : redirectURI ,
2022-03-07 03:01:39 +00:00
"authorization_response" : map [ string ] interface { } {
"type" : "authorization_response" ,
"response" : map [ string ] string {
"code" : code ,
"state" : state ,
} ,
2022-03-03 19:06:27 +00:00
} ,
} )
return
}
2022-10-16 15:16:54 +00:00
if responseType == constants . ResponseTypeToken {
2022-03-03 19:06:27 +00:00
// rollover the session for security
2022-06-29 16:54:00 +00:00
authToken , err := token . CreateAuthToken ( gc , user , claims . Roles , scope , claims . LoginMethod )
2022-03-03 19:06:27 +00:00
if err != nil {
2022-10-16 15:16:54 +00:00
log . Debug ( "CreateAuthToken failed: " , err )
handleResponse ( gc , responseMode , loginURL , redirectURI , loginError , http . StatusOK )
return
}
if err := memorystore . Provider . SetUserSession ( sessionKey , constants . TokenTypeSessionToken + "_" + authToken . FingerPrint , 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 + "_" + authToken . FingerPrint , authToken . AccessToken . Token ) ; err != nil {
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-03-08 14:01:19 +00:00
cookie . SetSession ( gc , authToken . FingerPrintHash )
2022-03-25 12:21:20 +00:00
expiresIn := authToken . AccessToken . ExpiresAt - time . Now ( ) . Unix ( )
if expiresIn <= 0 {
expiresIn = 1
}
2022-03-08 07:06:26 +00:00
// used of query mode
params := "access_token=" + authToken . AccessToken . Token + "&token_type=bearer&expires_in=" + strconv . FormatInt ( expiresIn , 10 ) + "&state=" + state + "&id_token=" + authToken . IDToken . Token
2022-03-04 07:26:11 +00:00
res := map [ string ] interface { } {
"access_token" : authToken . AccessToken . Token ,
"id_token" : authToken . IDToken . Token ,
"state" : state ,
"scope" : scope ,
"token_type" : "Bearer" ,
"expires_in" : expiresIn ,
}
if authToken . RefreshToken != nil {
res [ "refresh_token" ] = authToken . RefreshToken . Token
2022-03-08 07:06:26 +00:00
params += "&refresh_token=" + authToken . RefreshToken . Token
2022-06-29 16:54:00 +00:00
memorystore . Provider . SetUserSession ( sessionKey , constants . TokenTypeRefreshToken + "_" + authToken . FingerPrint , authToken . RefreshToken . Token )
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 {
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 {
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 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
}
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
if val , ok := data [ "error" ] ; ok {
if val == "login_required" || val == "signup_required" {
isAuthenticationRequired = true
}
}
switch responseMode {
case constants . ResponseModeQuery , constants . ResponseModeFragment :
if isAuthenticationRequired {
gc . Redirect ( http . StatusFound , loginURI )
} else {
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 :
gc . HTML ( httpStatusCode , authorizeFormPostTemplate , gin . H {
"target_origin" : redirectURI ,
"authorization_response" : data ,
} )
return
}
}