authorizer/server/handlers/oauth_callback.go

487 lines
16 KiB
Go
Raw Normal View History

package handlers
import (
2021-12-03 17:25:27 +00:00
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
2022-05-23 06:22:51 +00:00
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
2021-07-23 16:27:44 +00:00
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/cookie"
2021-07-23 16:27:44 +00:00
"github.com/authorizerdev/authorizer/server/db"
2022-01-21 08:04:04 +00:00
"github.com/authorizerdev/authorizer/server/db/models"
2022-05-27 17:50:38 +00:00
"github.com/authorizerdev/authorizer/server/memorystore"
2021-07-23 16:27:44 +00:00
"github.com/authorizerdev/authorizer/server/oauth"
"github.com/authorizerdev/authorizer/server/token"
2021-07-23 16:27:44 +00:00
"github.com/authorizerdev/authorizer/server/utils"
)
2022-01-17 06:02:13 +00:00
// OAuthCallbackHandler handles the OAuth callback for various oauth providers
func OAuthCallbackHandler() gin.HandlerFunc {
return func(c *gin.Context) {
provider := c.Param("oauth_provider")
state := c.Request.FormValue("state")
2022-05-29 11:52:46 +00:00
sessionState, err := memorystore.Provider.GetState(state)
if sessionState == "" || err != nil {
2022-05-25 07:00:22 +00:00
log.Debug("Invalid oauth state: ", state)
2022-01-17 06:02:13 +00:00
c.JSON(400, gin.H{"error": "invalid oauth state"})
}
// contains random token, redirect url, role
2022-03-08 13:43:45 +00:00
sessionSplit := strings.Split(state, "___")
2022-01-17 06:02:13 +00:00
if len(sessionSplit) < 3 {
2022-05-25 07:00:22 +00:00
log.Debug("Unable to get redirect url from state: ", state)
2022-01-17 06:02:13 +00:00
c.JSON(400, gin.H{"error": "invalid redirect url"})
return
}
2022-06-11 18:57:21 +00:00
// remove state from store
go memorystore.Provider.RemoveState(state)
stateValue := sessionSplit[0]
2022-01-17 06:02:13 +00:00
redirectURL := sessionSplit[1]
inputRoles := strings.Split(sessionSplit[2], ",")
scopes := strings.Split(sessionSplit[3], ",")
2022-01-17 06:02:13 +00:00
2022-01-21 08:04:04 +00:00
user := models.User{}
2022-01-17 06:02:13 +00:00
code := c.Request.FormValue("code")
switch provider {
case constants.SignupMethodGoogle:
user, err = processGoogleUserInfo(code)
case constants.SignupMethodGithub:
user, err = processGithubUserInfo(code)
case constants.SignupMethodFacebook:
user, err = processFacebookUserInfo(code)
2022-06-06 16:38:32 +00:00
case constants.SignupMethodLinkedIn:
user, err = processLinkedInUserInfo(code)
case constants.SignupMethodApple:
user, err = processAppleUserInfo(code)
2022-01-17 06:02:13 +00:00
default:
2022-05-23 06:22:51 +00:00
log.Info("Invalid oauth provider")
2022-01-17 06:02:13 +00:00
err = fmt.Errorf(`invalid oauth provider`)
}
if err != nil {
2022-05-23 06:22:51 +00:00
log.Debug("Failed to process user info: ", err)
2022-01-17 06:02:13 +00:00
c.JSON(400, gin.H{"error": err.Error()})
return
}
2022-01-21 08:04:04 +00:00
existingUser, err := db.Provider.GetUserByEmail(user.Email)
2022-05-23 06:22:51 +00:00
log := log.WithField("user", user.Email)
2022-01-17 06:02:13 +00:00
if err != nil {
2022-05-29 11:52:46 +00:00
isSignupDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp)
if err != nil {
log.Debug("Failed to get signup disabled env variable: ", err)
c.JSON(400, gin.H{"error": err.Error()})
return
}
if isSignupDisabled {
2022-05-23 06:22:51 +00:00
log.Debug("Failed to signup as disabled")
2022-03-16 17:19:18 +00:00
c.JSON(400, gin.H{"error": "signup is disabled for this instance"})
return
}
2022-01-17 06:02:13 +00:00
// user not registered, register user and generate session token
user.SignupMethods = provider
// make sure inputRoles don't include protected roles
hasProtectedRole := false
for _, ir := range inputRoles {
2022-05-31 02:44:03 +00:00
protectedRolesString, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyProtectedRoles)
protectedRoles := []string{}
2022-05-29 11:52:46 +00:00
if err != nil {
log.Debug("Failed to get protected roles: ", err)
2022-05-31 02:44:03 +00:00
protectedRolesString = ""
} else {
protectedRoles = strings.Split(protectedRolesString, ",")
2022-05-29 11:52:46 +00:00
}
if utils.StringSliceContains(protectedRoles, ir) {
2022-01-17 06:02:13 +00:00
hasProtectedRole = true
}
}
if hasProtectedRole {
2022-05-25 07:00:22 +00:00
log.Debug("Signup is not allowed with protected roles:", inputRoles)
2022-01-17 06:02:13 +00:00
c.JSON(400, gin.H{"error": "invalid role"})
return
}
user.Roles = strings.Join(inputRoles, ",")
now := time.Now().Unix()
user.EmailVerifiedAt = &now
2022-01-21 08:04:04 +00:00
user, _ = db.Provider.AddUser(user)
2022-01-17 06:02:13 +00:00
} else {
2022-06-11 18:57:21 +00:00
user = existingUser
if user.RevokedTimestamp != nil {
2022-05-25 07:00:22 +00:00
log.Debug("User access revoked at: ", user.RevokedTimestamp)
c.JSON(400, gin.H{"error": "user access has been revoked"})
2022-06-11 18:57:21 +00:00
return
}
2022-01-17 06:02:13 +00:00
// user exists in db, check if method was google
// if not append google to existing signup method and save it
signupMethod := existingUser.SignupMethods
if !strings.Contains(signupMethod, provider) {
signupMethod = signupMethod + "," + provider
}
user.SignupMethods = signupMethod
2022-01-31 06:05:24 +00:00
if user.EmailVerifiedAt == nil {
now := time.Now().Unix()
user.EmailVerifiedAt = &now
}
2022-01-17 06:02:13 +00:00
// There multiple scenarios with roles here in social login
// 1. user has access to protected roles + roles and trying to login
// 2. user has not signed up for one of the available role but trying to signup.
// Need to modify roles in this case
// find the unassigned roles
existingRoles := strings.Split(existingUser.Roles, ",")
unasignedRoles := []string{}
for _, ir := range inputRoles {
if !utils.StringSliceContains(existingRoles, ir) {
unasignedRoles = append(unasignedRoles, ir)
}
}
if len(unasignedRoles) > 0 {
// check if it contains protected unassigned role
hasProtectedRole := false
for _, ur := range unasignedRoles {
2022-05-31 02:44:03 +00:00
protectedRolesString, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyProtectedRoles)
protectedRoles := []string{}
2022-05-29 11:52:46 +00:00
if err != nil {
log.Debug("Failed to get protected roles: ", err)
2022-05-31 02:44:03 +00:00
protectedRolesString = ""
} else {
protectedRoles = strings.Split(protectedRolesString, ",")
2022-05-29 11:52:46 +00:00
}
if utils.StringSliceContains(protectedRoles, ur) {
2022-01-17 06:02:13 +00:00
hasProtectedRole = true
}
}
if hasProtectedRole {
2022-05-23 06:22:51 +00:00
log.Debug("Invalid role. User is using protected unassigned role")
2022-01-17 06:02:13 +00:00
c.JSON(400, gin.H{"error": "invalid role"})
return
} else {
user.Roles = existingUser.Roles + "," + strings.Join(unasignedRoles, ",")
}
} else {
user.Roles = existingUser.Roles
}
2022-02-02 07:00:11 +00:00
2022-01-21 08:04:04 +00:00
user, err = db.Provider.UpdateUser(user)
2022-02-02 07:00:11 +00:00
if err != nil {
2022-05-23 06:22:51 +00:00
log.Debug("Failed to update user: ", err)
2022-02-02 07:00:11 +00:00
c.JSON(500, gin.H{"error": err.Error()})
return
}
2022-01-17 06:02:13 +00:00
}
authToken, err := token.CreateAuthToken(c, user, inputRoles, scopes)
2022-03-07 03:01:39 +00:00
if err != nil {
2022-05-23 06:22:51 +00:00
log.Debug("Failed to create auth token: ", err)
2022-03-07 03:01:39 +00:00
c.JSON(500, gin.H{"error": err.Error()})
}
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
if expiresIn <= 0 {
expiresIn = 1
}
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + stateValue + "&id_token=" + authToken.IDToken.Token
cookie.SetSession(c, authToken.FingerPrintHash)
2022-06-11 18:57:21 +00:00
memorystore.Provider.SetUserSession(user.ID, constants.TokenTypeSessionToken+"_"+authToken.FingerPrint, authToken.FingerPrintHash)
memorystore.Provider.SetUserSession(user.ID, constants.TokenTypeAccessToken+"_"+authToken.FingerPrint, authToken.AccessToken.Token)
if authToken.RefreshToken != nil {
2022-03-08 14:01:19 +00:00
params = params + `&refresh_token=` + authToken.RefreshToken.Token
2022-06-11 18:57:21 +00:00
memorystore.Provider.SetUserSession(user.ID, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token)
}
2022-01-17 06:02:13 +00:00
go db.Provider.AddSession(models.Session{
UserID: user.ID,
UserAgent: utils.GetUserAgent(c.Request),
IP: utils.GetIP(c.Request),
})
if strings.Contains(redirectURL, "?") {
redirectURL = redirectURL + "&" + params
} else {
2022-06-05 17:16:56 +00:00
redirectURL = redirectURL + "?" + strings.TrimPrefix(params, "&")
}
2022-01-17 06:02:13 +00:00
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
}
}
2022-01-21 08:04:04 +00:00
func processGoogleUserInfo(code string) (models.User, error) {
user := models.User{}
2021-12-03 17:25:27 +00:00
ctx := context.Background()
oauth2Token, err := oauth.OAuthProviders.GoogleConfig.Exchange(ctx, code)
if err != nil {
2022-05-23 06:22:51 +00:00
log.Debug("Failed to exchange code for token: ", err)
return user, fmt.Errorf("invalid google exchange code: %s", err.Error())
}
2021-12-03 17:25:27 +00:00
verifier := oauth.OIDCProviders.GoogleOIDC.Verifier(&oidc.Config{ClientID: oauth.OAuthProviders.GoogleConfig.ClientID})
// Extract the ID Token from OAuth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
2022-05-25 07:00:22 +00:00
log.Debug("Failed to extract ID Token from OAuth2 token")
2021-12-03 17:25:27 +00:00
return user, fmt.Errorf("unable to extract id_token")
}
2021-12-03 17:25:27 +00:00
// Parse and verify ID Token payload.
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
2022-05-23 06:22:51 +00:00
log.Debug("Failed to verify ID Token: ", err)
return user, fmt.Errorf("unable to verify id_token: %s", err.Error())
}
if err := idToken.Claims(&user); err != nil {
2022-05-23 06:22:51 +00:00
log.Debug("Failed to parse ID Token claims: ", err)
2021-12-03 17:25:27 +00:00
return user, fmt.Errorf("unable to extract claims")
}
return user, nil
2021-07-17 23:18:42 +00:00
}
2022-01-21 08:04:04 +00:00
func processGithubUserInfo(code string) (models.User, error) {
user := models.User{}
oauth2Token, err := oauth.OAuthProviders.GithubConfig.Exchange(oauth2.NoContext, code)
2021-07-17 23:18:42 +00:00
if err != nil {
2022-05-25 07:00:22 +00:00
log.Debug("Failed to exchange code for token: ", err)
return user, fmt.Errorf("invalid github exchange code: %s", err.Error())
2021-07-17 23:18:42 +00:00
}
client := http.Client{}
req, err := http.NewRequest("GET", constants.GithubUserInfoURL, nil)
if err != nil {
2022-05-25 07:00:22 +00:00
log.Debug("Failed to create github user info request: ", err)
return user, fmt.Errorf("error creating github user info request: %s", err.Error())
2021-07-17 23:18:42 +00:00
}
req.Header = http.Header{
"Authorization": []string{fmt.Sprintf("token %s", oauth2Token.AccessToken)},
}
2021-07-17 23:18:42 +00:00
response, err := client.Do(req)
if err != nil {
2022-05-25 07:00:22 +00:00
log.Debug("Failed to request github user info: ", err)
return user, err
2021-07-17 23:18:42 +00:00
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
2022-05-25 07:00:22 +00:00
log.Debug("Failed to read github user info response body: ", err)
return user, fmt.Errorf("failed to read github response body: %s", err.Error())
2021-07-17 23:18:42 +00:00
}
2022-06-06 16:38:32 +00:00
if response.StatusCode >= 400 {
log.Debug("Failed to request github user info: ", string(body))
return user, fmt.Errorf("failed to request github user info: %s", string(body))
2022-06-06 16:38:32 +00:00
}
2021-07-17 23:18:42 +00:00
userRawData := make(map[string]string)
json.Unmarshal(body, &userRawData)
name := strings.Split(userRawData["name"], " ")
firstName := ""
lastName := ""
if len(name) >= 1 && strings.TrimSpace(name[0]) != "" {
firstName = name[0]
}
if len(name) > 1 && strings.TrimSpace(name[1]) != "" {
lastName = name[0]
}
2021-12-22 10:01:45 +00:00
picture := userRawData["avatar_url"]
2022-01-21 08:04:04 +00:00
user = models.User{
2021-12-22 10:01:45 +00:00
GivenName: &firstName,
FamilyName: &lastName,
Picture: &picture,
2022-04-22 14:26:55 +00:00
Email: userRawData["email"],
2021-07-17 23:18:42 +00:00
}
return user, nil
}
2022-01-21 08:04:04 +00:00
func processFacebookUserInfo(code string) (models.User, error) {
user := models.User{}
oauth2Token, err := oauth.OAuthProviders.FacebookConfig.Exchange(oauth2.NoContext, code)
2021-09-04 22:27:29 +00:00
if err != nil {
2022-05-25 07:00:22 +00:00
log.Debug("Invalid facebook exchange code: ", err)
return user, fmt.Errorf("invalid facebook exchange code: %s", err.Error())
2021-09-04 22:27:29 +00:00
}
client := http.Client{}
req, err := http.NewRequest("GET", constants.FacebookUserInfoURL+oauth2Token.AccessToken, nil)
2021-09-04 22:27:29 +00:00
if err != nil {
2022-05-25 07:00:22 +00:00
log.Debug("Error creating facebook user info request: ", err)
return user, fmt.Errorf("error creating facebook user info request: %s", err.Error())
2021-09-04 22:27:29 +00:00
}
response, err := client.Do(req)
if err != nil {
2022-05-25 07:00:22 +00:00
log.Debug("Failed to process facebook user: ", err)
return user, err
2021-09-04 22:27:29 +00:00
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
2022-05-25 07:00:22 +00:00
log.Debug("Failed to read facebook response: ", err)
return user, fmt.Errorf("failed to read facebook response body: %s", err.Error())
2021-09-04 22:27:29 +00:00
}
2022-06-06 16:38:32 +00:00
if response.StatusCode >= 400 {
log.Debug("Failed to request facebook user info: ", string(body))
return user, fmt.Errorf("failed to request facebook user info: %s", string(body))
2022-06-06 16:38:32 +00:00
}
2021-09-04 22:27:29 +00:00
userRawData := make(map[string]interface{})
json.Unmarshal(body, &userRawData)
2022-03-02 12:12:31 +00:00
email := fmt.Sprintf("%v", userRawData["sub"])
2021-09-04 22:27:29 +00:00
picObject := userRawData["picture"].(map[string]interface{})["data"]
picDataObject := picObject.(map[string]interface{})
2021-12-22 10:01:45 +00:00
firstName := fmt.Sprintf("%v", userRawData["first_name"])
lastName := fmt.Sprintf("%v", userRawData["last_name"])
picture := fmt.Sprintf("%v", picDataObject["url"])
2022-01-21 08:04:04 +00:00
user = models.User{
2021-12-22 10:01:45 +00:00
GivenName: &firstName,
FamilyName: &lastName,
Picture: &picture,
Email: email,
2021-09-04 22:27:29 +00:00
}
return user, nil
2021-09-04 22:27:29 +00:00
}
2022-06-06 16:38:32 +00:00
func processLinkedInUserInfo(code string) (models.User, error) {
user := models.User{}
oauth2Token, err := oauth.OAuthProviders.LinkedInConfig.Exchange(oauth2.NoContext, code)
2022-06-06 16:38:32 +00:00
if err != nil {
log.Debug("Failed to exchange code for token: ", err)
return user, fmt.Errorf("invalid linkedin exchange code: %s", err.Error())
}
client := http.Client{}
req, err := http.NewRequest("GET", constants.LinkedInUserInfoURL, nil)
if err != nil {
log.Debug("Failed to create linkedin user info request: ", err)
return user, fmt.Errorf("error creating linkedin user info request: %s", err.Error())
}
req.Header = http.Header{
"Authorization": []string{fmt.Sprintf("Bearer %s", oauth2Token.AccessToken)},
2022-06-06 16:38:32 +00:00
}
response, err := client.Do(req)
if err != nil {
log.Debug("Failed to request linkedin user info: ", err)
return user, err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Debug("Failed to read linkedin user info response body: ", err)
return user, fmt.Errorf("failed to read linkedin response body: %s", err.Error())
}
if response.StatusCode >= 400 {
log.Debug("Failed to request linkedin user info: ", string(body))
return user, fmt.Errorf("failed to request linkedin user info: %s", string(body))
}
userRawData := make(map[string]interface{})
json.Unmarshal(body, &userRawData)
req, err = http.NewRequest("GET", constants.LinkedInEmailURL, nil)
if err != nil {
log.Debug("Failed to create linkedin email info request: ", err)
return user, fmt.Errorf("error creating linkedin user info request: %s", err.Error())
}
req.Header = http.Header{
"Authorization": []string{fmt.Sprintf("Bearer %s", oauth2Token.AccessToken)},
2022-06-06 16:38:32 +00:00
}
response, err = client.Do(req)
if err != nil {
log.Debug("Failed to request linkedin email info: ", err)
return user, err
}
defer response.Body.Close()
body, err = ioutil.ReadAll(response.Body)
if err != nil {
log.Debug("Failed to read linkedin email info response body: ", err)
return user, fmt.Errorf("failed to read linkedin email response body: %s", err.Error())
}
if response.StatusCode >= 400 {
log.Debug("Failed to request linkedin user info: ", string(body))
return user, fmt.Errorf("failed to request linkedin user info: %s", string(body))
}
emailRawData := make(map[string]interface{})
json.Unmarshal(body, &emailRawData)
firstName := userRawData["localizedFirstName"].(string)
lastName := userRawData["localizedLastName"].(string)
profilePicture := userRawData["profilePicture"].(map[string]interface{})["displayImage~"].(map[string]interface{})["elements"].([]interface{})[0].(map[string]interface{})["identifiers"].([]interface{})[0].(map[string]interface{})["identifier"].(string)
emailAddress := emailRawData["elements"].([]interface{})[0].(map[string]interface{})["handle~"].(map[string]interface{})["emailAddress"].(string)
user = models.User{
GivenName: &firstName,
FamilyName: &lastName,
Picture: &profilePicture,
Email: emailAddress,
}
return user, nil
}
func processAppleUserInfo(code string) (models.User, error) {
user := models.User{}
oauth2Token, err := oauth.OAuthProviders.AppleConfig.Exchange(oauth2.NoContext, code)
if err != nil {
log.Debug("Failed to exchange code for token: ", err)
return user, fmt.Errorf("invalid apple exchange code: %s", err.Error())
}
fmt.Println("=> token", oauth2Token.AccessToken)
// Extract the ID Token from OAuth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
log.Debug("Failed to extract ID Token from OAuth2 token")
return user, fmt.Errorf("unable to extract id_token")
}
fmt.Println("=> rawIDToken", rawIDToken)
// Parse and verify ID Token payload.
claims, err := token.ParseJWTToken(rawIDToken)
if err != nil {
log.Debug("Failed to parse apple id token: ", err)
return user, err
}
fmt.Println("claims:", claims)
email := claims["email"].(string)
user.Email = email
return user, err
}