git.iko.soy/typeable/oauth2-proxy
on custom-timeout help sign in
263 lines 7.1 kB history raw
1package providers
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io/ioutil"
8 "net/http"
9 "strings"
10 "time"
12 oidc "github.com/coreos/go-oidc"
13 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
14 "golang.org/x/oauth2"
17// GitLabProvider represents a GitLab based Identity Provider
18type GitLabProvider struct {
19 *ProviderData
21 Group string
22 EmailDomains []string
24 Verifier *oidc.IDTokenVerifier
25 AllowUnverifiedEmail bool
28var _ Provider = (*GitLabProvider)(nil)
30// NewGitLabProvider initiates a new GitLabProvider
31func NewGitLabProvider(p *ProviderData) *GitLabProvider {
32 p.ProviderName = "GitLab"
34 if p.Scope == "" {
35 p.Scope = "openid email"
36 }
38 return &GitLabProvider{ProviderData: p}
41// Redeem exchanges the OAuth2 authentication token for an ID token
42func (p *GitLabProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) {
43 clientSecret, err := p.GetClientSecret()
44 if err != nil {
45 return
46 }
48 c := oauth2.Config{
49 ClientID: p.ClientID,
50 ClientSecret: clientSecret,
51 Endpoint: oauth2.Endpoint{
52 TokenURL: p.RedeemURL.String(),
53 },
54 RedirectURL: redirectURL,
55 }
56 token, err := c.Exchange(ctx, code)
57 if err != nil {
58 return nil, fmt.Errorf("token exchange: %v", err)
59 }
60 s, err = p.createSessionState(ctx, token)
61 if err != nil {
62 return nil, fmt.Errorf("unable to update session: %v", err)
63 }
64 return
67// RefreshSessionIfNeeded checks if the session has expired and uses the
68// RefreshToken to fetch a new ID token if required
69func (p *GitLabProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
70 if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
71 return false, nil
72 }
74 origExpiration := s.ExpiresOn
76 err := p.redeemRefreshToken(ctx, s)
77 if err != nil {
78 return false, fmt.Errorf("unable to redeem refresh token: %v", err)
79 }
81 fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
82 return true, nil
85func (p *GitLabProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) (err error) {
86 clientSecret, err := p.GetClientSecret()
87 if err != nil {
88 return
89 }
91 c := oauth2.Config{
92 ClientID: p.ClientID,
93 ClientSecret: clientSecret,
94 Endpoint: oauth2.Endpoint{
95 TokenURL: p.RedeemURL.String(),
96 },
97 }
98 t := &oauth2.Token{
99 RefreshToken: s.RefreshToken,
100 Expiry: time.Now().Add(-time.Hour),
101 }
102 token, err := c.TokenSource(ctx, t).Token()
103 if err != nil {
104 return fmt.Errorf("failed to get token: %v", err)
105 }
106 newSession, err := p.createSessionState(ctx, token)
107 if err != nil {
108 return fmt.Errorf("unable to update session: %v", err)
109 }
110 s.AccessToken = newSession.AccessToken
111 s.IDToken = newSession.IDToken
112 s.RefreshToken = newSession.RefreshToken
113 s.CreatedAt = newSession.CreatedAt
114 s.ExpiresOn = newSession.ExpiresOn
115 s.Email = newSession.Email
116 return
119type gitlabUserInfo struct {
120 Username string `json:"nickname"`
121 Email string `json:"email"`
122 EmailVerified bool `json:"email_verified"`
123 Groups []string `json:"groups"`
126func (p *GitLabProvider) getUserInfo(ctx context.Context, s *sessions.SessionState) (*gitlabUserInfo, error) {
127 // Retrieve user info JSON
128 // https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information
130 // Build user info url from login url of GitLab instance
131 userInfoURL := *p.LoginURL
132 userInfoURL.Path = "/oauth/userinfo"
134 req, err := http.NewRequestWithContext(ctx, "GET", userInfoURL.String(), nil)
135 if err != nil {
136 return nil, fmt.Errorf("failed to create user info request: %v", err)
137 }
138 req.Header.Set("Authorization", "Bearer "+s.AccessToken)
140 resp, err := http.DefaultClient.Do(req)
141 if err != nil {
142 return nil, fmt.Errorf("failed to perform user info request: %v", err)
143 }
144 var body []byte
145 body, err = ioutil.ReadAll(resp.Body)
146 resp.Body.Close()
147 if err != nil {
148 return nil, fmt.Errorf("failed to read user info response: %v", err)
149 }
151 if resp.StatusCode != 200 {
152 return nil, fmt.Errorf("got %d during user info request: %s", resp.StatusCode, body)
153 }
155 var userInfo gitlabUserInfo
156 err = json.Unmarshal(body, &userInfo)
157 if err != nil {
158 return nil, fmt.Errorf("failed to parse user info: %v", err)
159 }
161 return &userInfo, nil
164func (p *GitLabProvider) verifyGroupMembership(userInfo *gitlabUserInfo) error {
165 if p.Group == "" {
166 return nil
167 }
169 // Collect user group memberships
170 membershipSet := make(map[string]bool)
171 for _, group := range userInfo.Groups {
172 membershipSet[group] = true
173 }
175 // Find a valid group that they are a member of
176 validGroups := strings.Split(p.Group, " ")
177 for _, validGroup := range validGroups {
178 if _, ok := membershipSet[validGroup]; ok {
179 return nil
180 }
181 }
183 return fmt.Errorf("user is not a member of '%s'", p.Group)
186func (p *GitLabProvider) verifyEmailDomain(userInfo *gitlabUserInfo) error {
187 if len(p.EmailDomains) == 0 || p.EmailDomains[0] == "*" {
188 return nil
189 }
191 for _, domain := range p.EmailDomains {
192 if strings.HasSuffix(userInfo.Email, domain) {
193 return nil
194 }
195 }
197 return fmt.Errorf("user email is not one of the valid domains '%v'", p.EmailDomains)
200func (p *GitLabProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
201 rawIDToken, ok := token.Extra("id_token").(string)
202 if !ok {
203 return nil, fmt.Errorf("token response did not contain an id_token")
204 }
206 // Parse and verify ID Token payload.
207 idToken, err := p.Verifier.Verify(ctx, rawIDToken)
208 if err != nil {
209 return nil, fmt.Errorf("could not verify id_token: %v", err)
210 }
212 return &sessions.SessionState{
213 AccessToken: token.AccessToken,
214 IDToken: rawIDToken,
215 RefreshToken: token.RefreshToken,
216 CreatedAt: time.Now(),
217 ExpiresOn: idToken.Expiry,
218 }, nil
221// ValidateSessionState checks that the session's IDToken is still valid
222func (p *GitLabProvider) ValidateSessionState(ctx context.Context, s *sessions.SessionState) bool {
223 _, err := p.Verifier.Verify(ctx, s.IDToken)
224 return err == nil
227// GetEmailAddress returns the Account email address
228func (p *GitLabProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
229 // Retrieve user info
230 userInfo, err := p.getUserInfo(ctx, s)
231 if err != nil {
232 return "", fmt.Errorf("failed to retrieve user info: %v", err)
233 }
235 // Check if email is verified
236 if !p.AllowUnverifiedEmail && !userInfo.EmailVerified {
237 return "", fmt.Errorf("user email is not verified")
238 }
240 // Check if email has valid domain
241 err = p.verifyEmailDomain(userInfo)
242 if err != nil {
243 return "", fmt.Errorf("email domain check failed: %v", err)
244 }
246 // Check group membership
247 err = p.verifyGroupMembership(userInfo)
248 if err != nil {
249 return "", fmt.Errorf("group membership check failed: %v", err)
250 }
252 return userInfo.Email, nil
255// GetUserName returns the Account user name
256func (p *GitLabProvider) GetUserName(ctx context.Context, s *sessions.SessionState) (string, error) {
257 userInfo, err := p.getUserInfo(ctx, s)
258 if err != nil {
259 return "", fmt.Errorf("failed to retrieve user info: %v", err)
260 }
262 return userInfo.Username, nil