git.iko.soy/typeable/oauth2-proxy
on 597ffeb help sign in
359 lines 9.9 kB history raw
1package providers
3import (
4 "context"
5 "fmt"
6 "net/url"
7 "strconv"
8 "strings"
9 "time"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
13 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
14 "golang.org/x/oauth2"
17// GitLabProvider represents a GitLab based Identity Provider
18type GitLabProvider struct {
19 *ProviderData
21 Groups []string
22 Projects []*GitlabProject
25// GitlabProject represents a Gitlab project constraint entity
26type GitlabProject struct {
27 Name string
28 AccessLevel int
31// newGitlabProject Creates a new GitlabProject struct from project string formatted as namespace/project=accesslevel
32// if no accesslevel provided, use the default one
33func newGitlabproject(project string) (*GitlabProject, error) {
34 // default access level is 20
35 defaultAccessLevel := 20
36 // see https://docs.gitlab.com/ee/api/members.html#valid-access-levels
37 validAccessLevel := [4]int{10, 20, 30, 40}
39 parts := strings.SplitN(project, "=", 2)
41 if len(parts) == 2 {
42 lvl, err := strconv.Atoi(parts[1])
43 if err != nil {
44 return nil, err
45 }
47 for _, valid := range validAccessLevel {
48 if lvl == valid {
49 return &GitlabProject{
50 Name: parts[0],
51 AccessLevel: lvl},
52 err
53 }
54 }
56 return nil, fmt.Errorf("invalid gitlab project access level specified (%s)", parts[0])
58 }
60 return &GitlabProject{
61 Name: project,
62 AccessLevel: defaultAccessLevel},
63 nil
67var _ Provider = (*GitLabProvider)(nil)
69const (
70 gitlabProviderName = "GitLab"
71 gitlabDefaultScope = "openid email"
74// NewGitLabProvider initiates a new GitLabProvider
75func NewGitLabProvider(p *ProviderData) *GitLabProvider {
76 p.ProviderName = gitlabProviderName
78 if p.Scope == "" {
79 p.Scope = gitlabDefaultScope
80 }
82 return &GitLabProvider{ProviderData: p}
85// Redeem exchanges the OAuth2 authentication token for an ID token
86func (p *GitLabProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) {
87 clientSecret, err := p.GetClientSecret()
88 if err != nil {
89 return
90 }
92 c := oauth2.Config{
93 ClientID: p.ClientID,
94 ClientSecret: clientSecret,
95 Endpoint: oauth2.Endpoint{
96 TokenURL: p.RedeemURL.String(),
97 },
98 RedirectURL: redirectURL,
99 }
100 token, err := c.Exchange(ctx, code)
101 if err != nil {
102 return nil, fmt.Errorf("token exchange: %v", err)
103 }
104 s, err = p.createSession(ctx, token)
105 if err != nil {
106 return nil, fmt.Errorf("unable to update session: %v", err)
107 }
108 return
111// SetProjectScope ensure read_api is added to scope when filtering on projects
112func (p *GitLabProvider) SetProjectScope() {
113 if len(p.Projects) > 0 {
114 for _, val := range strings.Split(p.Scope, " ") {
115 if val == "read_api" {
116 return
117 }
119 }
120 p.Scope += " read_api"
121 }
124// RefreshSessionIfNeeded checks if the session has expired and uses the
125// RefreshToken to fetch a new ID token if required
126func (p *GitLabProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
127 if s == nil || (s.ExpiresOn != nil && s.ExpiresOn.After(time.Now())) || s.RefreshToken == "" {
128 return false, nil
129 }
131 origExpiration := s.ExpiresOn
133 err := p.redeemRefreshToken(ctx, s)
134 if err != nil {
135 return false, fmt.Errorf("unable to redeem refresh token: %v", err)
136 }
138 fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
139 return true, nil
142func (p *GitLabProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) (err error) {
143 clientSecret, err := p.GetClientSecret()
144 if err != nil {
145 return
146 }
148 c := oauth2.Config{
149 ClientID: p.ClientID,
150 ClientSecret: clientSecret,
151 Endpoint: oauth2.Endpoint{
152 TokenURL: p.RedeemURL.String(),
153 },
154 }
155 t := &oauth2.Token{
156 RefreshToken: s.RefreshToken,
157 Expiry: time.Now().Add(-time.Hour),
158 }
159 token, err := c.TokenSource(ctx, t).Token()
160 if err != nil {
161 return fmt.Errorf("failed to get token: %v", err)
162 }
163 newSession, err := p.createSession(ctx, token)
164 if err != nil {
165 return fmt.Errorf("unable to update session: %v", err)
166 }
167 s.AccessToken = newSession.AccessToken
168 s.IDToken = newSession.IDToken
169 s.RefreshToken = newSession.RefreshToken
170 s.CreatedAt = newSession.CreatedAt
171 s.ExpiresOn = newSession.ExpiresOn
172 s.Email = newSession.Email
173 return
176type gitlabUserInfo struct {
177 Username string `json:"nickname"`
178 Email string `json:"email"`
179 EmailVerified bool `json:"email_verified"`
180 Groups []string `json:"groups"`
183func (p *GitLabProvider) getUserInfo(ctx context.Context, s *sessions.SessionState) (*gitlabUserInfo, error) {
184 // Retrieve user info JSON
185 // https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information
187 // Build user info url from login url of GitLab instance
188 userInfoURL := *p.LoginURL
189 userInfoURL.Path = "/oauth/userinfo"
191 var userInfo gitlabUserInfo
192 err := requests.New(userInfoURL.String()).
193 WithContext(ctx).
194 SetHeader("Authorization", "Bearer "+s.AccessToken).
195 Do().
196 UnmarshalInto(&userInfo)
197 if err != nil {
198 return nil, fmt.Errorf("error getting user info: %v", err)
199 }
201 return &userInfo, nil
204type gitlabPermissionAccess struct {
205 AccessLevel int `json:"access_level"`
208type gitlabProjectPermission struct {
209 ProjectAccess *gitlabPermissionAccess `json:"project_access"`
210 GroupAccess *gitlabPermissionAccess `json:"group_access"`
213type gitlabProjectInfo struct {
214 Name string `json:"name"`
215 Archived bool `json:"archived"`
216 PathWithNamespace string `json:"path_with_namespace"`
217 Permissions gitlabProjectPermission `json:"permissions"`
220func (p *GitLabProvider) getProjectInfo(ctx context.Context, s *sessions.SessionState, project string) (*gitlabProjectInfo, error) {
221 var projectInfo gitlabProjectInfo
223 endpointURL := &url.URL{
224 Scheme: p.LoginURL.Scheme,
225 Host: p.LoginURL.Host,
226 Path: "/api/v4/projects/",
227 }
229 err := requests.New(fmt.Sprintf("%s%s", endpointURL.String(), url.QueryEscape(project))).
230 WithContext(ctx).
231 SetHeader("Authorization", "Bearer "+s.AccessToken).
232 Do().
233 UnmarshalInto(&projectInfo)
235 if err != nil {
236 return nil, fmt.Errorf("failed to get project info: %v", err)
237 }
239 return &projectInfo, nil
242// AddProjects adds Gitlab projects from options to GitlabProvider struct
243func (p *GitLabProvider) AddProjects(projects []string) error {
244 for _, project := range projects {
245 gp, err := newGitlabproject(project)
246 if err != nil {
247 return err
248 }
250 p.Projects = append(p.Projects, gp)
251 }
253 return nil
256func (p *GitLabProvider) createSession(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
257 idToken, err := p.verifyIDToken(ctx, token)
258 if err != nil {
259 switch err {
260 case ErrMissingIDToken:
261 return nil, fmt.Errorf("token response did not contain an id_token")
262 default:
263 return nil, fmt.Errorf("could not verify id_token: %v", err)
264 }
265 }
267 created := time.Now()
268 return &sessions.SessionState{
269 AccessToken: token.AccessToken,
270 IDToken: getIDToken(token),
271 RefreshToken: token.RefreshToken,
272 CreatedAt: &created,
273 ExpiresOn: &idToken.Expiry,
274 }, nil
277// ValidateSession checks that the session's IDToken is still valid
278func (p *GitLabProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
279 _, err := p.Verifier.Verify(ctx, s.IDToken)
280 return err == nil
283// EnrichSession adds values and data from the Gitlab endpoint to current session
284func (p *GitLabProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
285 // Retrieve user info
286 userInfo, err := p.getUserInfo(ctx, s)
287 if err != nil {
288 return fmt.Errorf("failed to retrieve user info: %v", err)
289 }
291 // Check if email is verified
292 if !p.AllowUnverifiedEmail && !userInfo.EmailVerified {
293 return fmt.Errorf("user email is not verified")
294 }
296 s.User = userInfo.Username
297 s.Email = userInfo.Email
299 p.addGroupsToSession(ctx, s)
301 p.addProjectsToSession(ctx, s)
303 return nil
307// addGroupsToSession projects into session.Groups
308func (p *GitLabProvider) addGroupsToSession(ctx context.Context, s *sessions.SessionState) {
309 // Iterate over projects, check if oauth2-proxy can get project information on behalf of the user
310 for _, group := range p.Groups {
311 s.Groups = append(s.Groups, fmt.Sprintf("group:%s", group))
312 }
315// addProjectsToSession adds projects matching user access requirements into the session state groups list
316// This method prefix projects names with `project` to specify group kind
317func (p *GitLabProvider) addProjectsToSession(ctx context.Context, s *sessions.SessionState) {
318 // Iterate over projects, check if oauth2-proxy can get project information on behalf of the user
319 for _, project := range p.Projects {
320 projectInfo, err := p.getProjectInfo(ctx, s, project.Name)
322 if err != nil {
323 logger.Errorf("Warning: project info request failed: %v", err)
324 continue
325 }
327 if !projectInfo.Archived {
328 perms := projectInfo.Permissions.ProjectAccess
329 if perms == nil {
330 // use group project access as fallback
331 perms = projectInfo.Permissions.GroupAccess
332 }
334 if perms.AccessLevel >= project.AccessLevel {
335 s.Groups = append(s.Groups, fmt.Sprintf("project:%s", project.Name))
336 } else {
337 logger.Errorf("Warning: user %q does not have the minimum required access level for project %q", s.Email, project.Name)
338 }
339 } else {
340 logger.Errorf("Warning: project %s is archived", project.Name)
341 }
343 }
347// PrefixAllowedGroups returns a list of allowed groups, prefixed by their `kind` value
348func (p *GitLabProvider) PrefixAllowedGroups() (groups []string) {
350 for _, val := range p.Groups {
351 groups = append(groups, fmt.Sprintf("group:%s", val))
352 }
354 for _, val := range p.Projects {
355 groups = append(groups, fmt.Sprintf("project:%s", val.Name))
356 }
358 return groups