mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Auth: Support Gitlab OIDC scopes (#69890)
* draft gitlab openid * mutualize id token extraction * unexport fields * user user info endpoint for retrieving indirect group memberships * add to readme * fix missing doc * fix generic oauth wrong parameter * log token
This commit is contained in:
@@ -2,16 +2,12 @@ package social
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
@@ -223,60 +219,12 @@ func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson
|
||||
return nil
|
||||
}
|
||||
|
||||
jwtRegexp := regexp.MustCompile("^([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)$")
|
||||
matched := jwtRegexp.FindStringSubmatch(idToken.(string))
|
||||
if matched == nil {
|
||||
s.log.Debug("id_token is not in JWT format", "id_token", idToken.(string))
|
||||
return nil
|
||||
}
|
||||
|
||||
rawJSON, err := base64.RawURLEncoding.DecodeString(matched[2])
|
||||
rawJSON, err := s.retrieveRawIDToken(idToken)
|
||||
if err != nil {
|
||||
s.log.Error("Error base64 decoding id_token", "raw_payload", matched[2], "error", err)
|
||||
s.log.Warn("Error retrieving id_token", "error", err, "token", fmt.Sprintf("%+v", token))
|
||||
return nil
|
||||
}
|
||||
|
||||
headerBytes, err := base64.RawURLEncoding.DecodeString(matched[1])
|
||||
if err != nil {
|
||||
s.log.Error("Error base64 decoding header", "header", matched[1], "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var header map[string]interface{}
|
||||
if err := json.Unmarshal(headerBytes, &header); err != nil {
|
||||
s.log.Error("Error deserializing header", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if compressionVal, exists := header["zip"]; exists {
|
||||
compression, ok := compressionVal.(string)
|
||||
if !ok {
|
||||
s.log.Warn("Unknown compression algorithm")
|
||||
return nil
|
||||
}
|
||||
|
||||
if compression != "DEF" {
|
||||
s.log.Warn("Unknown compression algorithm", "algorithm", compression)
|
||||
return nil
|
||||
}
|
||||
|
||||
fr, err := zlib.NewReader(bytes.NewReader(rawJSON))
|
||||
if err != nil {
|
||||
s.log.Error("Error creating zlib reader", "error", err)
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if err := fr.Close(); err != nil {
|
||||
s.log.Warn("Failed closing zlib reader", "error", err)
|
||||
}
|
||||
}()
|
||||
rawJSON, err = io.ReadAll(fr)
|
||||
if err != nil {
|
||||
s.log.Error("Error decompressing payload", "error", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var data UserInfoJson
|
||||
if err := json.Unmarshal(rawJSON, &data); err != nil {
|
||||
s.log.Error("Error decoding id_token JSON", "raw_json", string(data.rawJSON), "error", err)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
@@ -19,7 +20,28 @@ type SocialGitlab struct {
|
||||
skipOrgRoleSync bool
|
||||
}
|
||||
|
||||
func (s *SocialGitlab) IsGroupMember(groups []string) bool {
|
||||
type apiData struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
State string `json:"state"`
|
||||
Name string `json:"name"`
|
||||
ConfirmedAt *string `json:"confirmed_at"` // "2020-10-02T09:39:40.882Z"
|
||||
}
|
||||
|
||||
type userData struct {
|
||||
ID string `json:"sub"`
|
||||
Login string `json:"preferred_username"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Groups []string `json:"groups_direct"`
|
||||
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Role roletype.RoleType `json:"-"`
|
||||
IsGrafanaAdmin *bool `json:"-"`
|
||||
}
|
||||
|
||||
func (s *SocialGitlab) isGroupMember(groups []string) bool {
|
||||
if len(s.allowedGroups) == 0 {
|
||||
return true
|
||||
}
|
||||
@@ -35,18 +57,18 @@ func (s *SocialGitlab) IsGroupMember(groups []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SocialGitlab) GetGroups(ctx context.Context, client *http.Client) []string {
|
||||
func (s *SocialGitlab) getGroups(ctx context.Context, client *http.Client) []string {
|
||||
groups := make([]string, 0)
|
||||
|
||||
for page, url := s.GetGroupsPage(ctx, client, s.apiUrl+"/groups"); page != nil; page, url = s.GetGroupsPage(ctx, client, url) {
|
||||
for page, url := s.getGroupsPage(ctx, client, s.apiUrl+"/groups"); page != nil; page, url = s.getGroupsPage(ctx, client, url) {
|
||||
groups = append(groups, page...)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
// GetGroupsPage returns groups and link to the next page if response is paginated
|
||||
func (s *SocialGitlab) GetGroupsPage(ctx context.Context, client *http.Client, url string) ([]string, string) {
|
||||
// getGroupsPage returns groups and link to the next page if response is paginated
|
||||
func (s *SocialGitlab) getGroupsPage(ctx context.Context, client *http.Client, url string) ([]string, string) {
|
||||
type Group struct {
|
||||
FullPath string `json:"full_path"`
|
||||
}
|
||||
@@ -87,60 +109,167 @@ func (s *SocialGitlab) GetGroupsPage(ctx context.Context, client *http.Client, u
|
||||
return fullPaths, next
|
||||
}
|
||||
|
||||
func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, _ *oauth2.Token) (*BasicUserInfo, error) {
|
||||
var data struct {
|
||||
Id int
|
||||
Username string
|
||||
Email string
|
||||
Name string
|
||||
State string
|
||||
func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
data, err := s.extractFromToken(ctx, client, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// fallback to API
|
||||
if data == nil {
|
||||
var errAPI error
|
||||
data, errAPI = s.extractFromAPI(ctx, client, token)
|
||||
if errAPI != nil {
|
||||
return nil, errAPI
|
||||
}
|
||||
}
|
||||
|
||||
userInfo := &BasicUserInfo{
|
||||
Id: data.ID,
|
||||
Name: data.Name,
|
||||
Login: data.Login,
|
||||
Email: data.Email,
|
||||
Groups: data.Groups,
|
||||
Role: data.Role,
|
||||
IsGrafanaAdmin: data.IsGrafanaAdmin,
|
||||
}
|
||||
|
||||
if !s.isGroupMember(data.Groups) {
|
||||
return nil, errMissingGroupMembership
|
||||
}
|
||||
|
||||
if s.allowAssignGrafanaAdmin && s.skipOrgRoleSync {
|
||||
s.log.Debug("allowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client, token *oauth2.Token) (*userData, error) {
|
||||
apiResp := &apiData{}
|
||||
response, err := s.httpGet(ctx, client, s.apiUrl+"/user")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error getting user info: %s", err)
|
||||
return nil, fmt.Errorf("Error getting user info: %w", err)
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(response.Body, &data); err != nil {
|
||||
return nil, fmt.Errorf("error getting user info: %s", err)
|
||||
if err = json.Unmarshal(response.Body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("error getting user info: %w", err)
|
||||
}
|
||||
|
||||
if data.State != "active" {
|
||||
return nil, fmt.Errorf("user %s is inactive", data.Username)
|
||||
// check confirmed_at exists and is not null
|
||||
if apiResp.ConfirmedAt == nil || *apiResp.ConfirmedAt == "" {
|
||||
return nil, fmt.Errorf("user %s's email is not confirmed", apiResp.Username)
|
||||
}
|
||||
|
||||
groups := s.GetGroups(ctx, client)
|
||||
if apiResp.State != "active" {
|
||||
return nil, fmt.Errorf("user %s is inactive", apiResp.Username)
|
||||
}
|
||||
|
||||
idData := &userData{
|
||||
ID: fmt.Sprintf("%d", apiResp.ID),
|
||||
Login: apiResp.Username,
|
||||
Email: apiResp.Email,
|
||||
Name: apiResp.Name,
|
||||
Groups: s.getGroups(ctx, client),
|
||||
}
|
||||
|
||||
var role roletype.RoleType
|
||||
var isGrafanaAdmin *bool = nil
|
||||
if !s.skipOrgRoleSync {
|
||||
var grafanaAdmin bool
|
||||
role, grafanaAdmin = s.extractRoleAndAdmin(response.Body, groups, true)
|
||||
role, grafanaAdmin := s.extractRoleAndAdmin(response.Body, idData.Groups, true)
|
||||
if s.roleAttributeStrict && !role.IsValid() {
|
||||
return nil, &InvalidBasicRoleError{idP: "Gitlab", assignedRole: string(role)}
|
||||
}
|
||||
|
||||
if s.allowAssignGrafanaAdmin {
|
||||
isGrafanaAdmin = &grafanaAdmin
|
||||
idData.IsGrafanaAdmin = &grafanaAdmin
|
||||
}
|
||||
}
|
||||
if s.allowAssignGrafanaAdmin && s.skipOrgRoleSync {
|
||||
s.log.Debug("allowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
|
||||
|
||||
idData.Role = role
|
||||
}
|
||||
|
||||
userInfo := &BasicUserInfo{
|
||||
Id: fmt.Sprintf("%d", data.Id),
|
||||
Name: data.Name,
|
||||
Login: data.Username,
|
||||
Email: data.Email,
|
||||
Groups: groups,
|
||||
Role: role,
|
||||
IsGrafanaAdmin: isGrafanaAdmin,
|
||||
}
|
||||
|
||||
if !s.IsGroupMember(groups) {
|
||||
return nil, errMissingGroupMembership
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
return idData, nil
|
||||
}
|
||||
|
||||
func (s *SocialGitlab) extractFromToken(ctx context.Context, client *http.Client, token *oauth2.Token) (*userData, error) {
|
||||
s.log.Debug("Extracting user info from OAuth token")
|
||||
|
||||
idToken := token.Extra("id_token")
|
||||
if idToken == nil {
|
||||
s.log.Debug("No id_token found, defaulting to API access", "token", token)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rawJSON, err := s.retrieveRawIDToken(idToken)
|
||||
if err != nil {
|
||||
s.log.Warn("Error retrieving id_token", "error", err, "token", fmt.Sprintf("%+v", idToken))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
s.log.Debug("Received id_token", "raw_json", string(rawJSON))
|
||||
var data userData
|
||||
if err := json.Unmarshal(rawJSON, &data); err != nil {
|
||||
s.log.Warn("Error decoding id_token JSON", "raw_json", string(rawJSON), "error", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// check email_verified
|
||||
if !data.EmailVerified {
|
||||
return nil, fmt.Errorf("user %s's email is not confirmed", data.Login)
|
||||
}
|
||||
|
||||
userInfo, err := s.retrieveUserInfo(ctx, client)
|
||||
if err != nil {
|
||||
s.log.Warn("Error retrieving groups from userinfo. Using only token provided groups", "error", err)
|
||||
} else {
|
||||
s.log.Debug("Retrieved groups from userinfo", "sub", userInfo.Sub,
|
||||
"original_groups", data.Groups, "groups", userInfo.Groups)
|
||||
data.Groups = userInfo.Groups
|
||||
}
|
||||
|
||||
if !s.skipOrgRoleSync {
|
||||
role, grafanaAdmin := s.extractRoleAndAdmin(rawJSON, data.Groups, true)
|
||||
if s.roleAttributeStrict && !role.IsValid() {
|
||||
return nil, &InvalidBasicRoleError{idP: "Gitlab", assignedRole: string(role)}
|
||||
}
|
||||
|
||||
if s.allowAssignGrafanaAdmin {
|
||||
data.IsGrafanaAdmin = &grafanaAdmin
|
||||
}
|
||||
|
||||
data.Role = role
|
||||
}
|
||||
|
||||
s.log.Debug("Resolved user data", "data", fmt.Sprintf("%+v", data))
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
type userInfoResponse struct {
|
||||
Sub string `json:"sub"`
|
||||
SubLegacy string `json:"sub_legacy"`
|
||||
Name string `json:"name"`
|
||||
Nickname string `json:"nickname"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Profile string `json:"profile"`
|
||||
Picture string `json:"picture"`
|
||||
Groups []string `json:"groups"`
|
||||
OwnerGroups []string `json:"https://gitlab.org/claims/groups/owner"`
|
||||
}
|
||||
|
||||
// retrieve and parse /oauth/userinfo
|
||||
func (s *SocialGitlab) retrieveUserInfo(ctx context.Context, client *http.Client) (*userInfoResponse, error) {
|
||||
userInfoURL := strings.TrimSuffix(s.Endpoint.AuthURL, "/oauth/authorize") + "/oauth/userinfo"
|
||||
|
||||
resp, err := s.httpGet(ctx, client, userInfoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var userInfo userInfoResponse
|
||||
if err := json.Unmarshal(resp.Body, &userInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
@@ -2,12 +2,17 @@ package social
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
)
|
||||
@@ -19,8 +24,8 @@ const (
|
||||
|
||||
gitlabAttrPath = `is_admin && 'GrafanaAdmin' || contains(groups[*], 'admins') && 'Admin' || contains(groups[*], 'editors') && 'Editor' || contains(groups[*], 'viewers') && 'Viewer'`
|
||||
|
||||
rootUserRespBody = `{"id":1,"username":"root","name":"Administrator","state":"active","email":"root@example.org","is_admin":true,"namespace_id":1}`
|
||||
editorUserRespBody = `{"id":3,"username":"gitlab-editor","name":"Gitlab Editor","state":"active","email":"gitlab-editor@example.org","is_admin":false,"namespace_id":1}`
|
||||
rootUserRespBody = `{"id":1,"username":"root","name":"Administrator","state":"active","email":"root@example.org", "confirmed_at":"2022-09-13T19:38:04.891Z","is_admin":true,"namespace_id":1}`
|
||||
editorUserRespBody = `{"id":3,"username":"gitlab-editor","name":"Gitlab Editor","state":"active","email":"gitlab-editor@example.org", "confirmed_at":"2022-09-13T19:38:04.891Z","is_admin":false,"namespace_id":1}`
|
||||
|
||||
adminGroup = `{"id":4,"web_url":"http://grafana-gitlab.local/groups/admins","name":"Admins","path":"admins","project_creation_level":"developer","full_name":"Admins","full_path":"admins","created_at":"2022-09-13T19:38:04.891Z"}`
|
||||
editorGroup = `{"id":5,"web_url":"http://grafana-gitlab.local/groups/editors","name":"Editors","path":"editors","project_creation_level":"developer","full_name":"Editors","full_path":"editors","created_at":"2022-09-13T19:38:15.074Z"}`
|
||||
@@ -160,7 +165,7 @@ func TestSocialGitlab_UserInfo(t *testing.T) {
|
||||
}
|
||||
}))
|
||||
provider.apiUrl = ts.URL + apiURI
|
||||
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), nil)
|
||||
actualResult, err := provider.UserInfo(context.Background(), ts.Client(), &oauth2.Token{})
|
||||
if test.ExpectedError != nil {
|
||||
require.Equal(t, err, test.ExpectedError)
|
||||
return
|
||||
@@ -174,3 +179,207 @@ func TestSocialGitlab_UserInfo(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
payload string
|
||||
config *oauth2.Config
|
||||
wantUser *userData
|
||||
wantErrMessage string
|
||||
}
|
||||
|
||||
func TestSocialGitlab_extractFromToken(t *testing.T) {
|
||||
// Create a test server that returns a dummy ID token and user info
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/oauth/token":
|
||||
// Return a dummy access token
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"access_token": "dummy_access_token",
|
||||
"token_type": "Bearer",
|
||||
})
|
||||
case "/oauth/userinfo":
|
||||
// Return a dummy user info
|
||||
_ = json.NewEncoder(w).Encode(userInfoResponse{
|
||||
Sub: "12345678",
|
||||
EmailVerified: true,
|
||||
Groups: []string{"admins", "editors", "viewers"},
|
||||
})
|
||||
default:
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "successful extraction",
|
||||
payload: `{
|
||||
"iss": "https://gitlab.com",
|
||||
"sub": "12345678",
|
||||
"aud": "d77db857f4696c5c5ff6cee64f3ed26e709aac8f1c644dc4b9d5fd64f825d583",
|
||||
"exp": 1686124040,
|
||||
"iat": 1686123920,
|
||||
"auth_time": 1686119303,
|
||||
"sub_legacy": "b4359d63eaf90d4b1f3d71d291353b75a676bf73fdf734d4ff009eca5c69bb70",
|
||||
"name": "John Doe",
|
||||
"nickname": "johndoe",
|
||||
"preferred_username": "johndoe",
|
||||
"email": "johndoe@example.com",
|
||||
"email_verified": true,
|
||||
"profile": "https://gitlab.com/johndoe",
|
||||
"picture": "https://gitlab.com/uploads/-/system/user/avatar/1234567/avatar.png",
|
||||
"groups_direct": [
|
||||
"admins"
|
||||
]
|
||||
}`,
|
||||
config: &oauth2.Config{
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: ts.URL + "/oauth/authorize",
|
||||
TokenURL: ts.URL + "/oauth/token",
|
||||
},
|
||||
},
|
||||
wantUser: &userData{
|
||||
ID: "12345678",
|
||||
Login: "johndoe",
|
||||
Email: "johndoe@example.com",
|
||||
Name: "John Doe",
|
||||
Groups: []string{"admins", "editors", "viewers"},
|
||||
EmailVerified: true,
|
||||
Role: "",
|
||||
IsGrafanaAdmin: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unverified email",
|
||||
payload: `{
|
||||
"iss": "https://gitlab.com",
|
||||
"sub": "12345678",
|
||||
"aud": "d77db857f4696c5c5ff6cee64f3ed26e709aac8f1c644dc4b9d5fd64f825d583",
|
||||
"exp": 1686124040,
|
||||
"iat": 1686123920,
|
||||
"auth_time": 1686119303,
|
||||
"sub_legacy": "b4359d63eaf90d4b1f3d71d291353b75a676bf73fdf734d4ff009eca5c69bb70",
|
||||
"name": "John Doe",
|
||||
"nickname": "johndoe",
|
||||
"preferred_username": "johndoe",
|
||||
"email": "johndoe@example.com",
|
||||
"email_verified": false,
|
||||
"profile": "https://gitlab.com/johndoe",
|
||||
"picture": "https://gitlab.com/uploads/-/system/user/avatar/1234567/avatar.png",
|
||||
"groups_direct": [
|
||||
"admins"
|
||||
]
|
||||
}`,
|
||||
config: &oauth2.Config{
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: ts.URL + "/oauth/authorize",
|
||||
TokenURL: ts.URL + "/oauth/token",
|
||||
},
|
||||
},
|
||||
wantErrMessage: "user johndoe's email is not confirmed",
|
||||
},
|
||||
{
|
||||
name: "unable to reach userinfo endpoint",
|
||||
payload: `{
|
||||
"iss": "https://gitlab.com",
|
||||
"sub": "12345678",
|
||||
"aud": "d77db857f4696c5c5ff6cee64f3ed26e709aac8f1c644dc4b9d5fd64f825d583",
|
||||
"exp": 1686124040,
|
||||
"iat": 1686123920,
|
||||
"auth_time": 1686119303,
|
||||
"sub_legacy": "b4359d63eaf90d4b1f3d71d291353b75a676bf73fdf734d4ff009eca5c69bb70",
|
||||
"name": "John Doe",
|
||||
"nickname": "johndoe",
|
||||
"preferred_username": "johndoe",
|
||||
"email": "johndoe@example.com",
|
||||
"email_verified": true,
|
||||
"profile": "https://gitlab.com/johndoe",
|
||||
"picture": "https://gitlab.com/uploads/-/system/user/avatar/1234567/avatar.png",
|
||||
"groups_direct": [
|
||||
"admins"
|
||||
]
|
||||
}`,
|
||||
config: &oauth2.Config{
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "http://localhost:1234/oauth/authorize",
|
||||
TokenURL: "http://localhost:1234/oauth/token",
|
||||
},
|
||||
},
|
||||
wantUser: &userData{
|
||||
ID: "12345678",
|
||||
Login: "johndoe",
|
||||
Email: "johndoe@example.com",
|
||||
Name: "John Doe",
|
||||
Groups: []string{"admins"},
|
||||
EmailVerified: true,
|
||||
Role: "",
|
||||
IsGrafanaAdmin: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create a test client with a dummy token
|
||||
client := oauth2.NewClient(context.Background(), &tokenSource{accessToken: "dummy_access_token"})
|
||||
|
||||
// Create a test SocialGitlab instance
|
||||
s := &SocialGitlab{
|
||||
SocialBase: &SocialBase{
|
||||
Config: tc.config,
|
||||
log: newLogger("test", "debug"),
|
||||
allowSignup: false,
|
||||
allowedDomains: []string{},
|
||||
roleAttributePath: "",
|
||||
roleAttributeStrict: false,
|
||||
autoAssignOrgRole: "",
|
||||
skipOrgRoleSync: false,
|
||||
},
|
||||
skipOrgRoleSync: false,
|
||||
}
|
||||
|
||||
// Test case: successful extraction
|
||||
token := &oauth2.Token{}
|
||||
// build jwt
|
||||
// header
|
||||
header := map[string]interface{}{
|
||||
"alg": "RS256",
|
||||
"typ": "JWT",
|
||||
"kid": "dummy",
|
||||
}
|
||||
headerJSON, err := json.Marshal(header)
|
||||
require.NoError(t, err)
|
||||
headerEncoded := base64.RawURLEncoding.EncodeToString(headerJSON)
|
||||
// payload
|
||||
payloadEncoded := base64.RawURLEncoding.EncodeToString([]byte(tc.payload))
|
||||
// signature
|
||||
signatureEncoded := base64.RawURLEncoding.EncodeToString([]byte("dummy"))
|
||||
// build token
|
||||
idToken := fmt.Sprintf("%s.%s.%s", headerEncoded, payloadEncoded, signatureEncoded)
|
||||
|
||||
token = token.WithExtra(map[string]interface{}{"id_token": idToken})
|
||||
data, err := s.extractFromToken(context.Background(), client, token)
|
||||
if tc.wantErrMessage != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.wantErrMessage)
|
||||
} else {
|
||||
require.NotNil(t, data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.wantUser, data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// tokenSource is a dummy oauth2.TokenSource that always returns a fixed access token
|
||||
type tokenSource struct {
|
||||
accessToken string
|
||||
}
|
||||
|
||||
func (t *tokenSource) Token() (*oauth2.Token, error) {
|
||||
return &oauth2.Token{
|
||||
AccessToken: t.accessToken,
|
||||
TokenType: "Bearer",
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@ package social
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -527,3 +531,59 @@ func (ss *SocialService) getUsageStats(ctx context.Context) (map[string]interfac
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *SocialBase) retrieveRawIDToken(idToken interface{}) ([]byte, error) {
|
||||
tokenString, ok := idToken.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("id_token is not a string: %v", idToken)
|
||||
}
|
||||
|
||||
jwtRegexp := regexp.MustCompile("^([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)$")
|
||||
matched := jwtRegexp.FindStringSubmatch(tokenString)
|
||||
if matched == nil {
|
||||
return nil, fmt.Errorf("id_token is not in JWT format: %s", tokenString)
|
||||
}
|
||||
|
||||
rawJSON, err := base64.RawURLEncoding.DecodeString(matched[2])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error base64 decoding id_token: %w", err)
|
||||
}
|
||||
|
||||
headerBytes, err := base64.RawURLEncoding.DecodeString(matched[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error base64 decoding header: %w", err)
|
||||
}
|
||||
|
||||
var header map[string]interface{}
|
||||
if err := json.Unmarshal(headerBytes, &header); err != nil {
|
||||
return nil, fmt.Errorf("error deserializing header: %w", err)
|
||||
}
|
||||
|
||||
if compressionVal, exists := header["zip"]; exists {
|
||||
compression, ok := compressionVal.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unrecognized compression header: %v", compressionVal)
|
||||
}
|
||||
|
||||
if compression != "DEF" {
|
||||
return nil, fmt.Errorf("unknown compression algorithm: %s", compression)
|
||||
}
|
||||
|
||||
fr, err := zlib.NewReader(bytes.NewReader(rawJSON))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating zlib reader: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := fr.Close(); err != nil {
|
||||
s.log.Warn("Failed closing zlib reader", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
rawJSON, err = io.ReadAll(fr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decompressing payload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return rawJSON, nil
|
||||
}
|
||||
|
||||
@@ -233,7 +233,8 @@ func (s *AuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *login.UpdateAut
|
||||
|
||||
return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
upd, err := sess.MustCols("o_auth_expiry").Where("user_id = ? AND auth_module = ?", cmd.UserId, cmd.AuthModule).Update(authUser)
|
||||
s.logger.Debug("Updated user_auth", "user_id", cmd.UserId, "auth_module", cmd.AuthModule, "rows", upd)
|
||||
s.logger.Debug("Updated user_auth", "user_id", cmd.UserId,
|
||||
"auth_id", cmd.AuthId, "auth_module", cmd.AuthModule, "rows", upd)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user