git.iko.soy/typeable/oauth2-proxy
on 71dc702 help sign in
244 lines 6.6 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/pkg/apis/sessions"
11 "github.com/oauth2-proxy/oauth2-proxy/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)
28// NewGitLabProvider initiates a new GitLabProvider
29func NewGitLabProvider(p *ProviderData) *GitLabProvider {
30 p.ProviderName = "GitLab"
32 if p.Scope == "" {
33 p.Scope = "openid email"
34 }
36 return &GitLabProvider{ProviderData: p}
39// Redeem exchanges the OAuth2 authentication token for an ID token
40func (p *GitLabProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) {
41 clientSecret, err := p.GetClientSecret()
42 if err != nil {
43 return
44 }
46 c := oauth2.Config{
47 ClientID: p.ClientID,
48 ClientSecret: clientSecret,
49 Endpoint: oauth2.Endpoint{
50 TokenURL: p.RedeemURL.String(),
51 },
52 RedirectURL: redirectURL,
53 }
54 token, err := c.Exchange(ctx, code)
55 if err != nil {
56 return nil, fmt.Errorf("token exchange: %v", err)
57 }
58 s, err = p.createSessionState(ctx, token)
59 if err != nil {
60 return nil, fmt.Errorf("unable to update session: %v", err)
61 }
62 return
65// RefreshSessionIfNeeded checks if the session has expired and uses the
66// RefreshToken to fetch a new ID token if required
67func (p *GitLabProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
68 if s == nil || (s.ExpiresOn != nil && s.ExpiresOn.After(time.Now())) || s.RefreshToken == "" {
69 return false, nil
70 }
72 origExpiration := s.ExpiresOn
74 err := p.redeemRefreshToken(ctx, s)
75 if err != nil {
76 return false, fmt.Errorf("unable to redeem refresh token: %v", err)
77 }
79 fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
80 return true, nil
83func (p *GitLabProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) (err error) {
84 clientSecret, err := p.GetClientSecret()
85 if err != nil {
86 return
87 }
89 c := oauth2.Config{
90 ClientID: p.ClientID,
91 ClientSecret: clientSecret,
92 Endpoint: oauth2.Endpoint{
93 TokenURL: p.RedeemURL.String(),
94 },
95 }
96 t := &oauth2.Token{
97 RefreshToken: s.RefreshToken,
98 Expiry: time.Now().Add(-time.Hour),
99 }
100 token, err := c.TokenSource(ctx, t).Token()
101 if err != nil {
102 return fmt.Errorf("failed to get token: %v", err)
103 }
104 newSession, err := p.createSessionState(ctx, token)
105 if err != nil {
106 return fmt.Errorf("unable to update session: %v", err)
107 }
108 s.AccessToken = newSession.AccessToken
109 s.IDToken = newSession.IDToken
110 s.RefreshToken = newSession.RefreshToken
111 s.CreatedAt = newSession.CreatedAt
112 s.ExpiresOn = newSession.ExpiresOn
113 s.Email = newSession.Email
114 return
117type gitlabUserInfo struct {
118 Username string `json:"nickname"`
119 Email string `json:"email"`
120 EmailVerified bool `json:"email_verified"`
121 Groups []string `json:"groups"`
124func (p *GitLabProvider) getUserInfo(ctx context.Context, s *sessions.SessionState) (*gitlabUserInfo, error) {
125 // Retrieve user info JSON
126 // https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information
128 // Build user info url from login url of GitLab instance
129 userInfoURL := *p.LoginURL
130 userInfoURL.Path = "/oauth/userinfo"
132 var userInfo gitlabUserInfo
133 err := requests.New(userInfoURL.String()).
134 WithContext(ctx).
135 SetHeader("Authorization", "Bearer "+s.AccessToken).
136 Do().
137 UnmarshalInto(&userInfo)
138 if err != nil {
139 return nil, fmt.Errorf("error getting user info: %v", err)
140 }
142 return &userInfo, nil
145func (p *GitLabProvider) verifyGroupMembership(userInfo *gitlabUserInfo) error {
146 if len(p.Groups) == 0 {
147 return nil
148 }
150 // Collect user group memberships
151 membershipSet := make(map[string]bool)
152 for _, group := range userInfo.Groups {
153 membershipSet[group] = true
154 }
156 // Find a valid group that they are a member of
157 for _, validGroup := range p.Groups {
158 if _, ok := membershipSet[validGroup]; ok {
159 return nil
160 }
161 }
163 return fmt.Errorf("user is not a member of '%s'", p.Groups)
166func (p *GitLabProvider) verifyEmailDomain(userInfo *gitlabUserInfo) error {
167 if len(p.EmailDomains) == 0 || p.EmailDomains[0] == "*" {
168 return nil
169 }
171 for _, domain := range p.EmailDomains {
172 if strings.HasSuffix(userInfo.Email, domain) {
173 return nil
174 }
175 }
177 return fmt.Errorf("user email is not one of the valid domains '%v'", p.EmailDomains)
180func (p *GitLabProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
181 rawIDToken, ok := token.Extra("id_token").(string)
182 if !ok {
183 return nil, fmt.Errorf("token response did not contain an id_token")
184 }
186 // Parse and verify ID Token payload.
187 idToken, err := p.Verifier.Verify(ctx, rawIDToken)
188 if err != nil {
189 return nil, fmt.Errorf("could not verify id_token: %v", err)
190 }
192 created := time.Now()
193 return &sessions.SessionState{
194 AccessToken: token.AccessToken,
195 IDToken: rawIDToken,
196 RefreshToken: token.RefreshToken,
197 CreatedAt: &created,
198 ExpiresOn: &idToken.Expiry,
199 }, nil
202// ValidateSessionState checks that the session's IDToken is still valid
203func (p *GitLabProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool {
204 _, err := p.Verifier.Verify(ctx, s.IDToken)
205 return err == nil
208// GetEmailAddress returns the Account email address
209func (p *GitLabProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
210 // Retrieve user info
211 userInfo, err := p.getUserInfo(ctx, s)
212 if err != nil {
213 return "", fmt.Errorf("failed to retrieve user info: %v", err)
214 }
216 // Check if email is verified
217 if !p.AllowUnverifiedEmail && !userInfo.EmailVerified {
218 return "", fmt.Errorf("user email is not verified")
219 }
221 // Check if email has valid domain
222 err = p.verifyEmailDomain(userInfo)
223 if err != nil {
224 return "", fmt.Errorf("email domain check failed: %v", err)
225 }
227 // Check group membership
228 err = p.verifyGroupMembership(userInfo)
229 if err != nil {
230 return "", fmt.Errorf("group membership check failed: %v", err)
231 }
233 return userInfo.Email, nil
236// GetUserName returns the Account user name
237func (p *GitLabProvider) GetUserName(ctx context.Context, s *sessions.SessionState) (string, error) {
238 userInfo, err := p.getUserInfo(ctx, s)
239 if err != nil {
240 return "", fmt.Errorf("failed to retrieve user info: %v", err)
241 }
243 return userInfo.Username, nil