git.iko.soy/typeable/oauth2-proxy
on remove-gap-headers help sign in
249 lines 6.7 kB history raw
1package providers
3import (
4 "context"
5 "fmt"
6 "strings"
7 "time"
9 oidc "github.com/coreos/go-oidc"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
12 "golang.org/x/oauth2"
15// GitLabProvider represents a GitLab based Identity Provider
16type GitLabProvider struct {
17 *ProviderData
19 Groups []string
20 EmailDomains []string
22 Verifier *oidc.IDTokenVerifier
23 AllowUnverifiedEmail bool
26var _ Provider = (*GitLabProvider)(nil)
28const (
29 gitlabProviderName = "GitLab"
30 gitlabDefaultScope = "openid email"
33// NewGitLabProvider initiates a new GitLabProvider
34func NewGitLabProvider(p *ProviderData) *GitLabProvider {
35 p.ProviderName = gitlabProviderName
37 if p.Scope == "" {
38 p.Scope = gitlabDefaultScope
39 }
41 return &GitLabProvider{ProviderData: p}
44// Redeem exchanges the OAuth2 authentication token for an ID token
45func (p *GitLabProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) {
46 clientSecret, err := p.GetClientSecret()
47 if err != nil {
48 return
49 }
51 c := oauth2.Config{
52 ClientID: p.ClientID,
53 ClientSecret: clientSecret,
54 Endpoint: oauth2.Endpoint{
55 TokenURL: p.RedeemURL.String(),
56 },
57 RedirectURL: redirectURL,
58 }
59 token, err := c.Exchange(ctx, code)
60 if err != nil {
61 return nil, fmt.Errorf("token exchange: %v", err)
62 }
63 s, err = p.createSessionState(ctx, token)
64 if err != nil {
65 return nil, fmt.Errorf("unable to update session: %v", err)
66 }
67 return
70// RefreshSessionIfNeeded checks if the session has expired and uses the
71// RefreshToken to fetch a new ID token if required
72func (p *GitLabProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
73 if s == nil || (s.ExpiresOn != nil && s.ExpiresOn.After(time.Now())) || s.RefreshToken == "" {
74 return false, nil
75 }
77 origExpiration := s.ExpiresOn
79 err := p.redeemRefreshToken(ctx, s)
80 if err != nil {
81 return false, fmt.Errorf("unable to redeem refresh token: %v", err)
82 }
84 fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
85 return true, nil
88func (p *GitLabProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) (err error) {
89 clientSecret, err := p.GetClientSecret()
90 if err != nil {
91 return
92 }
94 c := oauth2.Config{
95 ClientID: p.ClientID,
96 ClientSecret: clientSecret,
97 Endpoint: oauth2.Endpoint{
98 TokenURL: p.RedeemURL.String(),
99 },
100 }
101 t := &oauth2.Token{
102 RefreshToken: s.RefreshToken,
103 Expiry: time.Now().Add(-time.Hour),
104 }
105 token, err := c.TokenSource(ctx, t).Token()
106 if err != nil {
107 return fmt.Errorf("failed to get token: %v", err)
108 }
109 newSession, err := p.createSessionState(ctx, token)
110 if err != nil {
111 return fmt.Errorf("unable to update session: %v", err)
112 }
113 s.AccessToken = newSession.AccessToken
114 s.IDToken = newSession.IDToken
115 s.RefreshToken = newSession.RefreshToken
116 s.CreatedAt = newSession.CreatedAt
117 s.ExpiresOn = newSession.ExpiresOn
118 s.Email = newSession.Email
119 return
122type gitlabUserInfo struct {
123 Username string `json:"nickname"`
124 Email string `json:"email"`
125 EmailVerified bool `json:"email_verified"`
126 Groups []string `json:"groups"`
129func (p *GitLabProvider) getUserInfo(ctx context.Context, s *sessions.SessionState) (*gitlabUserInfo, error) {
130 // Retrieve user info JSON
131 // https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information
133 // Build user info url from login url of GitLab instance
134 userInfoURL := *p.LoginURL
135 userInfoURL.Path = "/oauth/userinfo"
137 var userInfo gitlabUserInfo
138 err := requests.New(userInfoURL.String()).
139 WithContext(ctx).
140 SetHeader("Authorization", "Bearer "+s.AccessToken).
141 Do().
142 UnmarshalInto(&userInfo)
143 if err != nil {
144 return nil, fmt.Errorf("error getting user info: %v", err)
145 }
147 return &userInfo, nil
150func (p *GitLabProvider) verifyGroupMembership(userInfo *gitlabUserInfo) error {
151 if len(p.Groups) == 0 {
152 return nil
153 }
155 // Collect user group memberships
156 membershipSet := make(map[string]bool)
157 for _, group := range userInfo.Groups {
158 membershipSet[group] = true
159 }
161 // Find a valid group that they are a member of
162 for _, validGroup := range p.Groups {
163 if _, ok := membershipSet[validGroup]; ok {
164 return nil
165 }
166 }
168 return fmt.Errorf("user is not a member of '%s'", p.Groups)
171func (p *GitLabProvider) verifyEmailDomain(userInfo *gitlabUserInfo) error {
172 if len(p.EmailDomains) == 0 || p.EmailDomains[0] == "*" {
173 return nil
174 }
176 for _, domain := range p.EmailDomains {
177 if strings.HasSuffix(userInfo.Email, domain) {
178 return nil
179 }
180 }
182 return fmt.Errorf("user email is not one of the valid domains '%v'", p.EmailDomains)
185func (p *GitLabProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
186 rawIDToken, ok := token.Extra("id_token").(string)
187 if !ok {
188 return nil, fmt.Errorf("token response did not contain an id_token")
189 }
191 // Parse and verify ID Token payload.
192 idToken, err := p.Verifier.Verify(ctx, rawIDToken)
193 if err != nil {
194 return nil, fmt.Errorf("could not verify id_token: %v", err)
195 }
197 created := time.Now()
198 return &sessions.SessionState{
199 AccessToken: token.AccessToken,
200 IDToken: rawIDToken,
201 RefreshToken: token.RefreshToken,
202 CreatedAt: &created,
203 ExpiresOn: &idToken.Expiry,
204 }, nil
207// ValidateSessionState checks that the session's IDToken is still valid
208func (p *GitLabProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool {
209 _, err := p.Verifier.Verify(ctx, s.IDToken)
210 return err == nil
213// GetEmailAddress returns the Account email address
214func (p *GitLabProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
215 // Retrieve user info
216 userInfo, err := p.getUserInfo(ctx, s)
217 if err != nil {
218 return "", fmt.Errorf("failed to retrieve user info: %v", err)
219 }
221 // Check if email is verified
222 if !p.AllowUnverifiedEmail && !userInfo.EmailVerified {
223 return "", fmt.Errorf("user email is not verified")
224 }
226 // Check if email has valid domain
227 err = p.verifyEmailDomain(userInfo)
228 if err != nil {
229 return "", fmt.Errorf("email domain check failed: %v", err)
230 }
232 // Check group membership
233 err = p.verifyGroupMembership(userInfo)
234 if err != nil {
235 return "", fmt.Errorf("group membership check failed: %v", err)
236 }
238 return userInfo.Email, nil
241// GetUserName returns the Account user name
242func (p *GitLabProvider) GetUserName(ctx context.Context, s *sessions.SessionState) (string, error) {
243 userInfo, err := p.getUserInfo(ctx, s)
244 if err != nil {
245 return "", fmt.Errorf("failed to retrieve user info: %v", err)
246 }
248 return userInfo.Username, nil