2022-03-04 07:26:11 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/base64"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
2022-03-25 12:21:20 +00:00
|
|
|
"time"
|
2022-03-04 07:26:11 +00:00
|
|
|
|
2022-05-23 06:22:51 +00:00
|
|
|
"github.com/gin-gonic/gin"
|
2022-10-23 15:38:08 +00:00
|
|
|
"github.com/google/uuid"
|
2022-05-23 06:22:51 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
|
2022-03-07 03:01:39 +00:00
|
|
|
"github.com/authorizerdev/authorizer/server/constants"
|
2022-03-04 07:26:11 +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-04 07:26:11 +00:00
|
|
|
"github.com/authorizerdev/authorizer/server/token"
|
|
|
|
)
|
|
|
|
|
2022-11-12 18:24:37 +00:00
|
|
|
type RequestBody struct {
|
|
|
|
CodeVerifier string `form:"code_verifier" json:"code_verifier"`
|
|
|
|
Code string `form:"code" json:"code"`
|
|
|
|
ClientID string `form:"client_id" json:"client_id"`
|
|
|
|
ClientSecret string `form:"client_secret" json:"client_secret"`
|
|
|
|
GrantType string `form:"grant_type" json:"grant_type"`
|
|
|
|
RefreshToken string `form:"refresh_token" json:"refresh_token"`
|
|
|
|
RedirectURI string `form:"redirect_uri" json:"redirect_uri"`
|
|
|
|
}
|
|
|
|
|
2022-03-08 13:19:42 +00:00
|
|
|
// TokenHandler to handle /oauth/token requests
|
2022-03-08 09:26:46 +00:00
|
|
|
// grant type required
|
2022-03-04 07:26:11 +00:00
|
|
|
func TokenHandler() gin.HandlerFunc {
|
|
|
|
return func(gc *gin.Context) {
|
2022-11-12 18:24:37 +00:00
|
|
|
var reqBody RequestBody
|
|
|
|
if err := gc.Bind(&reqBody); err != nil {
|
2022-05-23 06:22:51 +00:00
|
|
|
log.Debug("Error binding JSON: ", err)
|
2022-03-04 07:26:11 +00:00
|
|
|
gc.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "error_binding_json",
|
|
|
|
"error_description": err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-11-12 18:24:37 +00:00
|
|
|
codeVerifier := strings.TrimSpace(reqBody.CodeVerifier)
|
|
|
|
code := strings.TrimSpace(reqBody.Code)
|
|
|
|
clientID := strings.TrimSpace(reqBody.ClientID)
|
|
|
|
grantType := strings.TrimSpace(reqBody.GrantType)
|
|
|
|
refreshToken := strings.TrimSpace(reqBody.RefreshToken)
|
|
|
|
clientSecret := strings.TrimSpace(reqBody.ClientSecret)
|
2022-03-08 09:26:46 +00:00
|
|
|
|
|
|
|
if grantType == "" {
|
|
|
|
grantType = "authorization_code"
|
|
|
|
}
|
|
|
|
|
|
|
|
isRefreshTokenGrant := grantType == "refresh_token"
|
|
|
|
isAuthorizationCodeGrant := grantType == "authorization_code"
|
|
|
|
|
|
|
|
if !isRefreshTokenGrant && !isAuthorizationCodeGrant {
|
2022-05-25 07:00:22 +00:00
|
|
|
log.Debug("Invalid grant type: ", grantType)
|
2022-03-08 09:26:46 +00:00
|
|
|
gc.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "invalid_grant_type",
|
|
|
|
"error_description": "grant_type is invalid",
|
|
|
|
})
|
|
|
|
}
|
2022-03-07 03:01:39 +00:00
|
|
|
|
2022-11-16 17:10:45 +00:00
|
|
|
// check if clientID & clientSecret are present as part of
|
|
|
|
// authorization header with basic auth
|
2022-11-17 00:14:40 +00:00
|
|
|
if clientID == "" && clientSecret == "" {
|
2022-11-16 17:10:45 +00:00
|
|
|
clientID, clientSecret, _ = gc.Request.BasicAuth()
|
|
|
|
}
|
|
|
|
|
2022-03-07 03:01:39 +00:00
|
|
|
if clientID == "" {
|
2022-05-23 06:22:51 +00:00
|
|
|
log.Debug("Client ID is empty")
|
2022-03-07 03:01:39 +00:00
|
|
|
gc.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "client_id_required",
|
|
|
|
"error_description": "The client id is required",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-29 11:52:46 +00:00
|
|
|
if client, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyClientID); clientID != client || err != nil {
|
2022-05-25 07:00:22 +00:00
|
|
|
log.Debug("Client ID is invalid: ", clientID)
|
2022-03-07 03:01:39 +00:00
|
|
|
gc.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "invalid_client_id",
|
|
|
|
"error_description": "The client id is invalid",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-03-04 07:26:11 +00:00
|
|
|
|
2022-03-08 09:26:46 +00:00
|
|
|
var userID string
|
|
|
|
var roles, scope []string
|
2022-06-29 16:54:00 +00:00
|
|
|
loginMethod := ""
|
|
|
|
sessionKey := ""
|
|
|
|
|
2022-03-08 09:26:46 +00:00
|
|
|
if isAuthorizationCodeGrant {
|
|
|
|
if code == "" {
|
2022-05-23 06:22:51 +00:00
|
|
|
log.Debug("Code is empty")
|
2022-03-08 09:26:46 +00:00
|
|
|
gc.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "invalid_code",
|
|
|
|
"error_description": "The code is required",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-03-04 07:26:11 +00:00
|
|
|
|
2022-11-12 18:24:37 +00:00
|
|
|
if codeVerifier == "" && clientSecret == "" {
|
2022-03-08 09:26:46 +00:00
|
|
|
gc.JSON(http.StatusBadRequest, gin.H{
|
2022-11-12 18:24:37 +00:00
|
|
|
"error": "invalid_dat",
|
|
|
|
"error_description": "The code verifier or client secret is required",
|
2022-03-08 09:26:46 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-11-12 18:24:37 +00:00
|
|
|
// Get state
|
|
|
|
sessionData, err := memorystore.Provider.GetState(code)
|
|
|
|
if sessionData == "" || err != nil {
|
|
|
|
log.Debug("Session data is empty")
|
2022-03-08 09:26:46 +00:00
|
|
|
gc.JSON(http.StatusBadRequest, gin.H{
|
2022-11-12 18:24:37 +00:00
|
|
|
"error": "invalid_code",
|
|
|
|
"error_description": "The code is invalid",
|
2022-03-08 09:26:46 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-11-12 18:24:37 +00:00
|
|
|
// [0] -> code_challenge
|
|
|
|
// [1] -> session cookie
|
|
|
|
sessionDataSplit := strings.Split(sessionData, "@@")
|
|
|
|
|
|
|
|
go memorystore.Provider.RemoveState(code)
|
|
|
|
|
|
|
|
if codeVerifier != "" {
|
|
|
|
hash := sha256.New()
|
|
|
|
hash.Write([]byte(codeVerifier))
|
|
|
|
encryptedCode := strings.ReplaceAll(base64.RawURLEncoding.EncodeToString(hash.Sum(nil)), "+", "-")
|
|
|
|
encryptedCode = strings.ReplaceAll(encryptedCode, "/", "_")
|
|
|
|
encryptedCode = strings.ReplaceAll(encryptedCode, "=", "")
|
|
|
|
if encryptedCode != sessionDataSplit[0] {
|
|
|
|
gc.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "invalid_code_verifier",
|
|
|
|
"error_description": "The code verifier is invalid",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
if clientHash, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyClientSecret); clientSecret != clientHash || err != nil {
|
|
|
|
log.Debug("Client Secret is invalid: ", clientID)
|
|
|
|
gc.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "invalid_client_secret",
|
|
|
|
"error_description": "The client secret is invalid",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-08 09:26:46 +00:00
|
|
|
// validate session
|
|
|
|
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
|
|
|
|
if err != nil {
|
2022-05-23 06:22:51 +00:00
|
|
|
log.Debug("Error validating session: ", err)
|
2022-03-08 09:26:46 +00:00
|
|
|
gc.JSON(http.StatusUnauthorized, gin.H{
|
|
|
|
"error": "unauthorized",
|
|
|
|
"error_description": "Invalid session data",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-08-29 02:48:20 +00:00
|
|
|
|
2022-03-08 09:26:46 +00:00
|
|
|
userID = claims.Subject
|
|
|
|
roles = claims.Roles
|
|
|
|
scope = claims.Scope
|
2022-06-29 16:54:00 +00:00
|
|
|
loginMethod = claims.LoginMethod
|
2022-08-29 02:48:20 +00:00
|
|
|
|
2022-06-11 18:57:21 +00:00
|
|
|
// rollover the session for security
|
2022-06-29 16:54:00 +00:00
|
|
|
sessionKey = userID
|
|
|
|
if loginMethod != "" {
|
|
|
|
sessionKey = loginMethod + ":" + userID
|
|
|
|
}
|
2022-08-29 02:48:20 +00:00
|
|
|
|
2022-06-29 16:54:00 +00:00
|
|
|
go memorystore.Provider.DeleteUserSession(sessionKey, claims.Nonce)
|
2022-11-12 18:24:37 +00:00
|
|
|
|
2022-03-08 09:26:46 +00:00
|
|
|
} else {
|
|
|
|
// validate refresh token
|
|
|
|
if refreshToken == "" {
|
2022-05-23 06:22:51 +00:00
|
|
|
log.Debug("Refresh token is empty")
|
2022-03-08 09:26:46 +00:00
|
|
|
gc.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
"error": "invalid_refresh_token",
|
|
|
|
"error_description": "The refresh token is invalid",
|
|
|
|
})
|
2022-11-03 10:51:59 +00:00
|
|
|
return
|
2022-03-08 09:26:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
claims, err := token.ValidateRefreshToken(gc, refreshToken)
|
|
|
|
if err != nil {
|
2022-05-23 06:22:51 +00:00
|
|
|
log.Debug("Error validating refresh token: ", err)
|
2022-03-08 09:26:46 +00:00
|
|
|
gc.JSON(http.StatusUnauthorized, gin.H{
|
|
|
|
"error": "unauthorized",
|
|
|
|
"error_description": err.Error(),
|
|
|
|
})
|
2022-11-03 10:51:59 +00:00
|
|
|
return
|
2022-03-08 09:26:46 +00:00
|
|
|
}
|
|
|
|
userID = claims["sub"].(string)
|
2022-11-03 20:10:18 +00:00
|
|
|
claimLoginMethod := claims["login_method"]
|
2022-03-08 15:43:23 +00:00
|
|
|
rolesInterface := claims["roles"].([]interface{})
|
|
|
|
scopeInterface := claims["scope"].([]interface{})
|
|
|
|
for _, v := range rolesInterface {
|
|
|
|
roles = append(roles, v.(string))
|
|
|
|
}
|
|
|
|
for _, v := range scopeInterface {
|
|
|
|
scope = append(scope, v.(string))
|
|
|
|
}
|
2022-06-29 16:54:00 +00:00
|
|
|
|
|
|
|
sessionKey = userID
|
2022-11-03 20:10:18 +00:00
|
|
|
if claimLoginMethod != nil && claimLoginMethod != "" {
|
|
|
|
sessionKey = claimLoginMethod.(string) + ":" + sessionKey
|
|
|
|
loginMethod = claimLoginMethod.(string)
|
2022-06-29 16:54:00 +00:00
|
|
|
}
|
2022-11-03 20:10:18 +00:00
|
|
|
|
2022-03-08 13:48:33 +00:00
|
|
|
// remove older refresh token and rotate it for security
|
2022-06-29 16:54:00 +00:00
|
|
|
go memorystore.Provider.DeleteUserSession(sessionKey, claims["nonce"].(string))
|
|
|
|
}
|
|
|
|
|
|
|
|
if sessionKey == "" {
|
|
|
|
log.Debug("Error getting sessionKey: ", sessionKey, loginMethod)
|
|
|
|
gc.JSON(http.StatusUnauthorized, gin.H{
|
|
|
|
"error": "unauthorized",
|
|
|
|
"error_description": "User not found",
|
|
|
|
})
|
|
|
|
return
|
2022-03-04 07:26:11 +00:00
|
|
|
}
|
2022-03-08 09:26:46 +00:00
|
|
|
|
2022-07-10 16:19:33 +00:00
|
|
|
user, err := db.Provider.GetUserByID(gc, userID)
|
2022-03-04 07:26:11 +00:00
|
|
|
if err != nil {
|
2022-05-23 06:22:51 +00:00
|
|
|
log.Debug("Error getting user: ", err)
|
2022-03-04 07:26:11 +00:00
|
|
|
gc.JSON(http.StatusUnauthorized, gin.H{
|
|
|
|
"error": "unauthorized",
|
|
|
|
"error_description": "User not found",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-03-08 09:26:46 +00:00
|
|
|
|
2022-11-12 18:24:37 +00:00
|
|
|
nonce := uuid.New().String() + "@@" + code
|
2022-11-12 19:52:21 +00:00
|
|
|
authToken, err := token.CreateAuthToken(gc, user, roles, scope, loginMethod, nonce, code)
|
2022-03-04 07:26:11 +00:00
|
|
|
if err != nil {
|
2022-05-23 06:22:51 +00:00
|
|
|
log.Debug("Error creating auth token: ", err)
|
2022-03-04 07:26:11 +00:00
|
|
|
gc.JSON(http.StatusUnauthorized, gin.H{
|
|
|
|
"error": "unauthorized",
|
|
|
|
"error_description": "User not found",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-11-03 20:10:18 +00:00
|
|
|
|
2022-06-29 16:54:00 +00:00
|
|
|
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeSessionToken+"_"+authToken.FingerPrint, authToken.FingerPrintHash)
|
|
|
|
memorystore.Provider.SetUserSession(sessionKey, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token)
|
2022-03-04 07:26:11 +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-04 07:26:11 +00:00
|
|
|
res := map[string]interface{}{
|
|
|
|
"access_token": authToken.AccessToken.Token,
|
|
|
|
"id_token": authToken.IDToken.Token,
|
2022-03-08 09:26:46 +00:00
|
|
|
"scope": scope,
|
|
|
|
"roles": roles,
|
2022-03-04 07:26:11 +00:00
|
|
|
"expires_in": expiresIn,
|
|
|
|
}
|
|
|
|
|
|
|
|
if authToken.RefreshToken != nil {
|
|
|
|
res["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
|
|
|
}
|
|
|
|
|
|
|
|
gc.JSON(http.StatusOK, res)
|
|
|
|
}
|
|
|
|
}
|