git.iko.soy/typeable/oauth2-proxy
on 2dd4a96 help sign in
352 lines 9.8 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 logger.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
298 for _, group := range userInfo.Groups {
299 s.Groups = append(s.Groups, fmt.Sprintf("group:%s", group))
300 }
302 p.addProjectsToSession(ctx, s)
304 return nil
307// addProjectsToSession adds projects matching user access requirements into the session state groups list
308// This method prefix projects names with `project` to specify group kind
309func (p *GitLabProvider) addProjectsToSession(ctx context.Context, s *sessions.SessionState) {
310 // Iterate over projects, check if oauth2-proxy can get project information on behalf of the user
311 for _, project := range p.Projects {
312 projectInfo, err := p.getProjectInfo(ctx, s, project.Name)
314 if err != nil {
315 logger.Errorf("Warning: project info request failed: %v", err)
316 continue
317 }
319 if !projectInfo.Archived {
320 perms := projectInfo.Permissions.ProjectAccess
321 if perms == nil {
322 // use group project access as fallback
323 perms = projectInfo.Permissions.GroupAccess
324 // group project access is not set for this user then we give up
325 if perms == nil {
326 logger.Errorf("Warning: user %q has no project level access to %s", s.Email, project.Name)
327 continue
328 }
329 }
331 if perms != nil && perms.AccessLevel >= project.AccessLevel {
332 s.Groups = append(s.Groups, fmt.Sprintf("project:%s", project.Name))
333 } else {
334 logger.Errorf("Warning: user %q does not have the minimum required access level for project %q", s.Email, project.Name)
335 }
336 continue
337 }
339 logger.Errorf("Warning: project %s is archived", project.Name)
340 }
343// PrefixAllowedGroups returns a list of allowed groups, prefixed by their `kind` value
344func (p *GitLabProvider) PrefixAllowedGroups() (groups []string) {
345 for _, val := range p.Groups {
346 groups = append(groups, fmt.Sprintf("group:%s", val))
347 }
348 for _, val := range p.Projects {
349 groups = append(groups, fmt.Sprintf("project:%s", val.Name))
350 }
351 return groups