2022-02-28 15:56:49 +00:00
package handlers
import (
"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-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-02-28 15:56:49 +00:00
template := "authorize.tmpl"
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 == "" {
responseMode = "query"
2022-03-07 03:01:39 +00:00
}
2022-03-07 13:19:18 +00:00
if responseMode != "query" && responseMode != "web_message" {
2022-05-25 07:00:22 +00:00
log . Debug ( "Invalid response_mode: " , responseMode )
2022-03-07 13:19:18 +00:00
gc . JSON ( 400 , gin . H { "error" : "invalid response mode" } )
}
if redirectURI == "" {
redirectURI = "/app"
}
isQuery := responseMode == "query"
2022-03-08 07:06:26 +00:00
loginURL := "/app?state=" + state + "&scope=" + strings . Join ( scope , " " ) + "&redirect_uri=" + redirectURI
2022-03-07 13:19:18 +00:00
if clientID == "" {
if isQuery {
gc . Redirect ( http . StatusFound , loginURL )
} else {
2022-05-25 07:00:22 +00:00
log . Debug ( "Failed to get client_id: " , clientID )
2022-03-07 13:19:18 +00:00
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" ,
} ,
2022-03-07 03:01:39 +00:00
} ,
2022-03-07 13:19:18 +00:00
} )
}
2022-03-07 03:01:39 +00:00
return
}
2022-05-29 11:52:46 +00:00
if client , err := memorystore . Provider . GetStringStoreEnvVariable ( constants . EnvKeyClientID ) ; client != clientID || err != nil {
2022-03-07 13:19:18 +00:00
if isQuery {
gc . Redirect ( http . StatusFound , loginURL )
} else {
2022-05-25 07:00:22 +00:00
log . Debug ( "Invalid client_id: " , clientID )
2022-03-07 13:19:18 +00:00
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" ,
} ,
2022-03-03 19:06:27 +00:00
} ,
2022-03-07 13:19:18 +00:00
} )
}
2022-02-28 15:56:49 +00:00
return
}
if state == "" {
2022-03-07 13:19:18 +00:00
if isQuery {
gc . Redirect ( http . StatusFound , loginURL )
} else {
2022-05-25 07:00:22 +00:00
log . Debug ( "Failed to get state: " , state )
2022-03-07 13:19:18 +00:00
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" ,
} ,
2022-03-03 19:06:27 +00:00
} ,
2022-03-07 13:19:18 +00:00
} )
}
2022-02-28 15:56:49 +00:00
return
}
if responseType == "" {
2022-03-03 19:06:27 +00:00
responseType = "token"
2022-02-28 15:56:49 +00:00
}
2022-03-03 19:06:27 +00:00
isResponseTypeCode := responseType == "code"
isResponseTypeToken := responseType == "token"
2022-02-28 15:56:49 +00:00
2022-03-03 19:06:27 +00:00
if ! isResponseTypeCode && ! isResponseTypeToken {
2022-03-07 13:19:18 +00:00
if isQuery {
gc . Redirect ( http . StatusFound , loginURL )
} else {
2022-05-25 07:00:22 +00:00
log . Debug ( "Invalid response_type: " , responseType )
2022-03-07 13:19:18 +00:00
gc . HTML ( http . StatusOK , template , gin . H {
2022-03-07 03:01:39 +00:00
"target_origin" : redirectURI ,
2022-03-03 19:06:27 +00:00
"authorization_response" : map [ string ] interface { } {
"type" : "authorization_response" ,
"response" : map [ string ] string {
2022-03-07 13:19:18 +00:00
"error" : "response_type is invalid" ,
2022-03-03 19:06:27 +00:00
} ,
} ,
} )
2022-03-07 13:19:18 +00:00
}
return
}
if isResponseTypeCode {
if codeChallenge == "" {
if isQuery {
gc . Redirect ( http . StatusFound , loginURL )
} else {
2022-05-25 07:00:22 +00:00
log . Debug ( "Failed to get code_challenge: " , codeChallenge )
2022-03-07 13:19:18 +00:00
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" ,
} ,
} ,
} )
}
2022-03-03 19:06:27 +00:00
return
}
}
sessionToken , err := cookie . GetSession ( gc )
if err != nil {
2022-03-07 13:19:18 +00:00
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" ,
} ,
2022-03-03 19:06:27 +00:00
} ,
2022-03-07 13:19:18 +00:00
} )
}
2022-03-03 19:06:27 +00:00
return
}
// get session from cookie
claims , err := token . ValidateBrowserSession ( gc , sessionToken )
if err != nil {
2022-03-07 13:19:18 +00:00
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" ,
} ,
2022-03-03 19:06:27 +00:00
} ,
2022-03-07 13:19:18 +00:00
} )
}
2022-03-03 19:06:27 +00:00
return
}
userID := claims . Subject
user , err := db . Provider . GetUserByID ( userID )
if err != nil {
2022-03-07 13:19:18 +00:00
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" ,
} ,
2022-03-03 19:06:27 +00:00
} ,
2022-03-07 13:19:18 +00:00
} )
}
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-03-03 19:06:27 +00:00
// if user is logged in
2022-06-29 04:24:12 +00:00
// based on the response type code, generate the response
2022-03-03 19:06:27 +00:00
if isResponseTypeCode {
// rollover the session for security
2022-06-29 16:54:00 +00:00
go memorystore . Provider . DeleteUserSession ( sessionKey , claims . Nonce )
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-03-07 13:19:18 +00:00
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" ,
} ,
2022-03-03 19:06:27 +00:00
} ,
2022-03-07 13:19:18 +00:00
} )
}
2022-03-03 19:06:27 +00:00
return
}
2022-06-11 18:57:21 +00:00
memorystore . Provider . SetUserSession ( user . ID , constants . TokenTypeSessionToken + "_" + newSessionTokenData . Nonce , newSessionToken )
2022-03-03 19:06:27 +00:00
cookie . SetSession ( gc , newSessionToken )
code := uuid . New ( ) . String ( )
2022-05-27 17:50:38 +00:00
memorystore . Provider . SetState ( codeChallenge , code + "@" + newSessionToken )
2022-03-03 19:06:27 +00:00
gc . HTML ( http . StatusOK , template , gin . H {
"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
}
if isResponseTypeToken {
// 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-03-07 13:19:18 +00:00
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" ,
} ,
2022-03-03 19:06:27 +00:00
} ,
2022-03-07 13:19:18 +00:00
} )
}
2022-02-28 15:56:49 +00:00
return
}
2022-06-29 16:54:00 +00:00
go memorystore . Provider . DeleteUserSession ( sessionKey , claims . Nonce )
memorystore . Provider . SetUserSession ( sessionKey , constants . TokenTypeSessionToken + "_" + authToken . FingerPrint , authToken . FingerPrintHash )
memorystore . Provider . SetUserSession ( sessionKey , constants . TokenTypeAccessToken + "_" + authToken . FingerPrint , authToken . AccessToken . Token )
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-03-08 07:06:26 +00:00
if isQuery {
if strings . Contains ( redirectURI , "?" ) {
gc . Redirect ( http . StatusFound , redirectURI + "&" + params )
} else {
gc . Redirect ( http . StatusFound , redirectURI + "?" + params )
}
} else {
gc . HTML ( http . StatusOK , template , gin . H {
"target_origin" : redirectURI ,
"authorization_response" : map [ string ] interface { } {
"type" : "authorization_response" ,
"response" : res ,
} ,
} )
}
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-03-07 13:19:18 +00:00
if isQuery {
gc . Redirect ( http . StatusFound , loginURL )
} else {
// by default return with error
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" ,
} ,
2022-03-03 19:06:27 +00:00
} ,
2022-03-07 13:19:18 +00:00
} )
}
2022-02-28 15:56:49 +00:00
}
}