@@ -2,18 +2,17 @@ package providers
22
33import (
44 "context"
5− "encoding/json"
5+ "errors"
66 "fmt"
77 "reflect"
88 "strings"
99 "time"
1010
11− oidc "github.com/coreos/go-oidc"
12− "golang.org/x/oauth2"
13−
11+ "github.com/coreos/go-oidc"
1412 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1513 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1614 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
15+ "golang.org/x/oauth2"
1716)
1817
1918const emailClaim = "email"
@@ -23,7 +22,7 @@ type OIDCProvider struct {
2322 *ProviderData
2423
2524 AllowUnverifiedEmail bool
26− UserIDClaim string
25+ EmailClaim string
2726 GroupsClaim string
2827}
2928
@@ -36,10 +35,10 @@ func NewOIDCProvider(p *ProviderData) *OIDCProvider {
3635var _ Provider = (*OIDCProvider)(nil)
3736
3837// Redeem exchanges the OAuth2 authentication token for an ID token
39−func (p *OIDCProvider) Redeem(ctx context.Context, redirectURL, code string) (s *sessions.SessionState, err error) {
38+func (p *OIDCProvider) Redeem(ctx context.Context, redirectURL, code string) (*sessions.SessionState, error) {
4039 clientSecret, err := p.GetClientSecret()
4140 if err != nil {
42− return
41+ return nil, err
4342 }
4443
4544 c := oauth2.Config{
@@ -52,23 +51,74 @@ func (p *OIDCProvider) Redeem(ctx context.Context, redirectURL, code string) (s
5251 }
5352 token, err := c.Exchange(ctx, code)
5453 if err != nil {
55− return nil, fmt.Errorf("token exchange: %v", err)
54+ return nil, fmt.Errorf("token exchange failure: %v", err)
5655 }
5756
58− // in the initial exchange the id token is mandatory
59− idToken, err := p.findVerifiedIDToken(ctx, token)
60− if err != nil {
61− return nil, fmt.Errorf("could not verify id_token: %v", err)
62− } else if idToken == nil {
63− return nil, fmt.Errorf("token response did not contain an id_token")
57+ return p.createSession(ctx, token, false)
58+}
59+
60+// EnrichSessionState is called after Redeem to allow providers to enrich session fields
61+// such as User, Email, Groups with provider specific API calls.
62+func (p *OIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
63+ if p.ProfileURL.String() == "" {
64+ if s.Email == "" {
65+ return errors.New("id_token did not contain an email and profileURL is not defined")
66+ }
67+ return nil
68+ }
69+
70+ // Try to get missing emails or groups from a profileURL
71+ if s.Email == "" || len(s.Groups) == 0 {
72+ err := p.callProfileURL(ctx, s)
73+ if err != nil {
74+ logger.Errorf("Warning: Profile URL request failed: %v", err)
75+ }
76+ }
77+
78+ // If a mandatory email wasn't set, error at this point.
79+ if s.Email == "" {
80+ return errors.New("neither the id_token nor the profileURL set an email")
6481 }
82+ return nil
83+}
6584
66− s, err = p.createSessionState(ctx, token, idToken)
85+// callProfileURL enriches a session's Email & Groups via the JSON response of
86+// an OIDC profile URL
87+func (p *OIDCProvider) callProfileURL(ctx context.Context, s *sessions.SessionState) error {
88+ respJSON, err := requests.New(p.ProfileURL.String()).
89+ WithContext(ctx).
90+ WithHeaders(makeOIDCHeader(s.AccessToken)).
91+ Do().
92+ UnmarshalJSON()
6793 if err != nil {
68− return nil, fmt.Errorf("unable to update session: %v", err)
94+ return err
95+ }
96+
97+ email, err := respJSON.Get(p.EmailClaim).String()
98+ if err == nil && s.Email == "" {
99+ s.Email = email
100+ }
101+
102+ // Handle array & singleton groups cases
103+ if len(s.Groups) == 0 {
104+ groups, err := respJSON.Get(p.GroupsClaim).StringArray()
105+ if err == nil {
106+ s.Groups = groups
107+ } else {
108+ group, err := respJSON.Get(p.GroupsClaim).String()
109+ if err == nil {
110+ s.Groups = []string{group}
111+ }
112+ }
69113 }
70114
71− return
115+ return nil
116+}
117+
118+// ValidateSessionState checks that the session's IDToken is still valid
119+func (p *OIDCProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
120+ _, err := p.Verifier.Verify(ctx, s.IDToken)
121+ return err == nil
72122}
73123
74124// RefreshSessionIfNeeded checks if the session has expired and uses the
@@ -83,14 +133,16 @@ func (p *OIDCProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.S
83133 return false, fmt.Errorf("unable to redeem refresh token: %v", err)
84134 }
85135
86− fmt.Printf("refreshed access token %s (expired on %s)\n", s, s.ExpiresOn)
136+ logger.Printf("refreshed session: %s", s)
87137 return true, nil
88138}
89139
90−func (p *OIDCProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) (err error) {
140+// redeemRefreshToken uses a RefreshToken with the RedeemURL to refresh the
141+// Access Token and (probably) the ID Token.
142+func (p *OIDCProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) error {
91143 clientSecret, err := p.GetClientSecret()
92144 if err != nil {
93− return
145+ return err
94146 }
95147
96148 c := oauth2.Config{
@@ -109,19 +161,14 @@ func (p *OIDCProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sessi
109161 return fmt.Errorf("failed to get token: %v", err)
110162 }
111163
112− // in the token refresh response the id_token is optional
113− idToken, err := p.findVerifiedIDToken(ctx, token)
114− if err != nil {
115− return fmt.Errorf("unable to extract id_token from response: %v", err)
116− }
117−
118− newSession, err := p.createSessionState(ctx, token, idToken)
164+ newSession, err := p.createSession(ctx, token, true)
119165 if err != nil {
120166 return fmt.Errorf("unable create new session state from response: %v", err)
121167 }
122168
123− // It's possible that if the refresh token isn't in the token response the session will not contain an id token
124− // if it doesn't it's probably better to retain the old one
169+ // It's possible that if the refresh token isn't in the token response the
170+ // session will not contain an id token.
171+ // If it doesn't it's probably better to retain the old one
125172 if newSession.IDToken != "" {
126173 s.IDToken = newSession.IDToken
127174 s.Email = newSession.Email
@@ -135,102 +182,113 @@ func (p *OIDCProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sessi
135182 s.CreatedAt = newSession.CreatedAt
136183 s.ExpiresOn = newSession.ExpiresOn
137184
138− return
185+ return nil
139186}
140187
141−func (p *OIDCProvider) findVerifiedIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
142−
143− getIDToken := func() (string, bool) {
144− rawIDToken, _ := token.Extra("id_token").(string)
145− return rawIDToken, len(strings.TrimSpace(rawIDToken)) > 0
188+// CreateSessionFromToken converts Bearer IDTokens into sessions
189+func (p *OIDCProvider) CreateSessionFromToken(ctx context.Context, token string) (*sessions.SessionState, error) {
190+ idToken, err := p.Verifier.Verify(ctx, token)
191+ if err != nil {
192+ return nil, err
146193 }
147194
148− if rawIDToken, present := getIDToken(); present {
149− verifiedIDToken, err := p.Verifier.Verify(ctx, rawIDToken)
150− return verifiedIDToken, err
195+ ss, err := p.buildSessionFromClaims(idToken)
196+ if err != nil {
197+ return nil, err
151198 }
152− return nil, nil
153−}
154−
155−func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Token, idToken *oidc.IDToken) (*sessions.SessionState, error) {
156−
157− var newSession *sessions.SessionState
158199
159− if idToken == nil {
160− newSession = &sessions.SessionState{}
161− } else {
162− var err error
163− newSession, err = p.createSessionStateInternal(ctx, idToken, token)
164− if err != nil {
165− return nil, err
166− }
200+ // Allow empty Email in Bearer case since we can't hit the ProfileURL
201+ if ss.Email == "" {
202+ ss.Email = ss.User
167203 }
168204
169− created := time.Now()
170− newSession.AccessToken = token.AccessToken
171− newSession.RefreshToken = token.RefreshToken
172− newSession.CreatedAt = &created
173− newSession.ExpiresOn = &token.Expiry
174− return newSession, nil
205+ ss.AccessToken = token
206+ ss.IDToken = token
207+ ss.RefreshToken = ""
208+ ss.ExpiresOn = &idToken.Expiry
209+
210+ return ss, nil
175211}
176212
177−func (p *OIDCProvider) CreateSessionFromToken(ctx context.Context, token string) (*sessions.SessionState, error) {
178− idToken, err := p.Verifier.Verify(ctx, token)
213+// createSession takes an oauth2.Token and creates a SessionState from it.
214+// It alters behavior if called from Redeem vs Refresh
215+func (p *OIDCProvider) createSession(ctx context.Context, token *oauth2.Token, refresh bool) (*sessions.SessionState, error) {
216+ idToken, err := p.findVerifiedIDToken(ctx, token)
179217 if err != nil {
180− return nil, err
218+ return nil, fmt.Errorf("could not verify id_token: %v", err)
219+ }
220+
221+ // IDToken is mandatory in Redeem but optional in Refresh
222+ if idToken == nil && !refresh {
223+ return nil, errors.New("token response did not contain an id_token")
181224 }
182225
183− newSession, err := p.createSessionStateInternal(ctx, idToken, nil)
226+ ss, err := p.buildSessionFromClaims(idToken)
184227 if err != nil {
185228 return nil, err
186229 }
187230
188− newSession.AccessToken = token
189− newSession.IDToken = token
190− newSession.RefreshToken = ""
191− newSession.ExpiresOn = &idToken.Expiry
231+ ss.AccessToken = token.AccessToken
232+ ss.RefreshToken = token.RefreshToken
233+ ss.IDToken = getIDToken(token)
192234
193− return newSession, nil
235+ created := time.Now()
236+ ss.CreatedAt = &created
237+ ss.ExpiresOn = &token.Expiry
238+
239+ return ss, nil
194240}
195241
196−func (p *OIDCProvider) createSessionStateInternal(ctx context.Context, idToken *oidc.IDToken, token *oauth2.Token) (*sessions.SessionState, error) {
242+func (p *OIDCProvider) findVerifiedIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
243+ rawIDToken := getIDToken(token)
244+ if strings.TrimSpace(rawIDToken) != "" {
245+ return p.Verifier.Verify(ctx, rawIDToken)
246+ }
247+ return nil, nil
248+}
197249
198− newSession := &sessions.SessionState{}
250+// buildSessionFromClaims uses IDToken claims to populate a fresh SessionState
251+// with non-Token related fields.
252+func (p *OIDCProvider) buildSessionFromClaims(idToken *oidc.IDToken) (*sessions.SessionState, error) {
253+ ss := &sessions.SessionState{}
199254
200255 if idToken == nil {
201− return newSession, nil
256+ return ss, nil
202257 }
203258
204− claims, err := p.findClaimsFromIDToken(ctx, idToken, token)
259+ claims, err := p.getClaims(idToken)
205260 if err != nil {
206261 return nil, fmt.Errorf("couldn't extract claims from id_token (%v)", err)
207262 }
208263
209− if token != nil {
210− newSession.IDToken = token.Extra("id_token").(string)
211− }
212−
213− newSession.Email = claims.UserID // TODO Rename SessionState.Email to .UserID in the near future
264+ ss.User = claims.Subject
265+ ss.Email = claims.Email
266+ ss.Groups = claims.Groups
214267
215− newSession.User = claims.Subject
216− newSession.Groups = claims.Groups
217− newSession.PreferredUsername = claims.PreferredUsername
268+ // TODO (@NickMeves) Deprecate for dynamic claim to session mapping
269+ if pref, ok := claims.rawClaims["preferred_username"].(string); ok {
270+ ss.PreferredUsername = pref
271+ }
218272
219− verifyEmail := (p.UserIDClaim == emailClaim) && !p.AllowUnverifiedEmail
273+ verifyEmail := (p.EmailClaim == emailClaim) && !p.AllowUnverifiedEmail
220274 if verifyEmail && claims.Verified != nil && !*claims.Verified {
221− return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.UserID)
275+ return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
222276 }
223277
224− return newSession, nil
278+ return ss, nil
225279}
226280
227−// ValidateSessionState checks that the session's IDToken is still valid
228−func (p *OIDCProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
229− _, err := p.Verifier.Verify(ctx, s.IDToken)
230− return err == nil
281+type OIDCClaims struct {
282+ Subject string `json:"sub"`
283+ Email string `json:"-"`
284+ Groups []string `json:"-"`
285+ Verified *bool `json:"email_verified"`
286+
287+ rawClaims map[string]interface{}
231288}
232289
233−func (p *OIDCProvider) findClaimsFromIDToken(ctx context.Context, idToken *oidc.IDToken, token *oauth2.Token) (*OIDCClaims, error) {
290+// getClaims extracts IDToken claims into an OIDCClaims
291+func (p *OIDCProvider) getClaims(idToken *oidc.IDToken) (*OIDCClaims, error) {
234292 claims := &OIDCClaims{}
235293
236294 // Extract default claims.
@@ -242,86 +300,28 @@ func (p *OIDCProvider) findClaimsFromIDToken(ctx context.Context, idToken *oidc.
242300 return nil, fmt.Errorf("failed to parse all id_token claims: %v", err)
243301 }
244302
245− userID := claims.rawClaims[p.UserIDClaim]
246− if userID != nil {
247− claims.UserID = fmt.Sprint(userID)
248− }
249−
250− claims.Groups = p.extractGroupsFromRawClaims(claims.rawClaims)
251−
252− // userID claim was not present or was empty in the ID Token
253− if claims.UserID == "" {
254− // BearerToken case, allow empty UserID
255− // ProfileURL checks below won't work since we don't have an access token
256− if token == nil {
257− claims.UserID = claims.Subject
258− return claims, nil
259− }
260−
261− profileURL := p.ProfileURL.String()
262− if profileURL == "" || token.AccessToken == "" {
263− return nil, fmt.Errorf("id_token did not contain user ID claim (%q)", p.UserIDClaim)
264− }
265−
266− // If the userinfo endpoint profileURL is defined, then there is a chance the userinfo
267− // contents at the profileURL contains the email.
268− // Make a query to the userinfo endpoint, and attempt to locate the email from there.
269− respJSON, err := requests.New(profileURL).
270− WithContext(ctx).
271− WithHeaders(makeOIDCHeader(token.AccessToken)).
272− Do().
273− UnmarshalJSON()
274− if err != nil {
275− return nil, err
276− }
277−
278− userID, err := respJSON.Get(p.UserIDClaim).String()
279− if err != nil {
280− return nil, fmt.Errorf("neither id_token nor userinfo endpoint contained user ID claim (%q)", p.UserIDClaim)
281− }
282−
283− claims.UserID = userID
303+ email := claims.rawClaims[p.EmailClaim]
304+ if email != nil {
305+ claims.Email = fmt.Sprint(email)
284306 }
307+ claims.Groups = p.extractGroups(claims.rawClaims)
285308
286309 return claims, nil
287310}
288311
289−func (p *OIDCProvider) extractGroupsFromRawClaims(rawClaims map[string]interface{}) []string {
312+func (p *OIDCProvider) extractGroups(claims map[string]interface{}) []string {
290313 groups := []string{}
291−
292− rawGroups, ok := rawClaims[p.GroupsClaim].([]interface{})
314+ rawGroups, ok := claims[p.GroupsClaim].([]interface{})
293315 if rawGroups != nil && ok {
294316 for _, rawGroup := range rawGroups {
295317 formattedGroup, err := formatGroup(rawGroup)
296318 if err != nil {
297− logger.Errorf("unable to format group of type %s with error %s", reflect.TypeOf(rawGroup), err)
319+ logger.Errorf("Warning: unable to format group of type %s with error %s",
320+ reflect.TypeOf(rawGroup), err)
298321 continue
299322 }
300323 groups = append(groups, formattedGroup)
301324 }
302325 }
303−
304326 return groups
305−
306−}
307−
308−func formatGroup(rawGroup interface{}) (string, error) {
309− group, ok := rawGroup.(string)
310− if !ok {
311− jsonGroup, err := json.Marshal(rawGroup)
312− if err != nil {
313− return "", err
314− }
315− group = string(jsonGroup)
316− }
317− return group, nil
318−}
319−
320−type OIDCClaims struct {
321− rawClaims map[string]interface{}
322− UserID string
323− Subject string `json:"sub"`
324− Verified *bool `json:"email_verified"`
325− PreferredUsername string `json:"preferred_username"`
326− Groups []string `json:"-"`
327327}