git.iko.soy/typeable/oauth2-proxy
on master help sign in
269 lines 7.6 kB history raw
1package providers
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net/url"
8 "time"
10 "github.com/coreos/go-oidc/v3/oidc"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
13 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr"
16 "golang.org/x/oauth2"
19// OIDCProvider represents an OIDC based Identity Provider
20type OIDCProvider struct {
21 *ProviderData
23 SkipNonce bool
26const oidcDefaultScope = "openid email profile"
28// NewOIDCProvider initiates a new OIDCProvider
29func NewOIDCProvider(p *ProviderData, opts options.OIDCOptions) *OIDCProvider {
30 name := "OpenID Connect"
32 if p.ProviderName != "" {
33 name = p.ProviderName
34 }
36 oidcProviderDefaults := providerDefaults{
37 name: name,
38 loginURL: nil,
39 redeemURL: nil,
40 profileURL: nil,
41 validateURL: nil,
42 scope: oidcDefaultScope,
43 }
45 if len(p.AllowedGroups) > 0 {
46 oidcProviderDefaults.scope += " groups"
47 }
49 p.setProviderDefaults(oidcProviderDefaults)
50 p.getAuthorizationHeaderFunc = makeOIDCHeader
52 return &OIDCProvider{
53 ProviderData: p,
54 SkipNonce: ptr.Deref(opts.InsecureSkipNonce, options.DefaultInsecureSkipNonce),
55 }
58var _ Provider = (*OIDCProvider)(nil)
60// GetLoginURL makes the LoginURL with optional nonce support
61func (p *OIDCProvider) GetLoginURL(redirectURI, state, nonce string, extraParams url.Values) string {
62 if !p.SkipNonce {
63 extraParams.Add("nonce", nonce)
64 }
65 // Response mode should only be set if a non default mode is requested
66 if p.AuthRequestResponseMode != "" {
67 extraParams.Add("response_mode", p.AuthRequestResponseMode)
68 }
70 loginURL := makeLoginURL(p.Data(), redirectURI, state, extraParams)
71 return loginURL.String()
74// Redeem exchanges the OAuth2 authentication token for an ID token
75func (p *OIDCProvider) Redeem(ctx context.Context, redirectURL, code, codeVerifier string) (*sessions.SessionState, error) {
76 clientSecret, err := p.GetClientSecret()
77 if err != nil {
78 return nil, err
79 }
81 var opts []oauth2.AuthCodeOption
82 if codeVerifier != "" {
83 opts = append(opts, oauth2.SetAuthURLParam("code_verifier", codeVerifier))
84 }
86 c := oauth2.Config{
87 ClientID: p.ClientID,
88 ClientSecret: clientSecret,
89 Endpoint: oauth2.Endpoint{
90 TokenURL: p.RedeemURL.String(),
91 },
92 RedirectURL: redirectURL,
93 }
95 ctx = oidc.ClientContext(ctx, requests.DefaultHTTPClient)
96 token, err := c.Exchange(ctx, code, opts...)
97 if err != nil {
98 return nil, fmt.Errorf("token exchange failed: %v", err)
99 }
101 return p.createSession(ctx, token, false)
104// EnrichSession is called after Redeem to allow providers to enrich session fields
105// such as User, Email, Groups with provider specific API calls.
106func (p *OIDCProvider) EnrichSession(_ context.Context, s *sessions.SessionState) error {
107 // If a mandatory email wasn't set, error at this point.
108 if s.Email == "" {
109 return errors.New("neither the id_token nor the profileURL set an email")
110 }
111 return nil
114// ValidateSession checks that the session's id_token or access_token (when a ValidateURL is configured) is still valid
115func (p *OIDCProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
116 ctx = oidc.ClientContext(ctx, requests.DefaultHTTPClient)
118 // https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
119 // The ID Token is optional in the Refresh Token Response
120 if s.Refreshed {
121 validateEndpointAvailable := p.Data().ValidateURL != nil && p.Data().ValidateURL.String() != ""
122 if validateEndpointAvailable && !validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken)) {
123 logger.Errorf("access_token validation failed")
124 return false
125 }
126 return true
127 }
129 if _, err := p.Verifier.Verify(ctx, s.IDToken); err != nil {
130 logger.Errorf("id_token verification failed: %v", err)
131 return false
132 }
134 if p.SkipNonce {
135 return true
136 }
138 if err := p.checkNonce(s); err != nil {
139 logger.Errorf("nonce verification failed: %v", err)
140 return false
141 }
143 return true
146// RefreshSession uses the RefreshToken to fetch new Access and ID Tokens
147func (p *OIDCProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
148 if s == nil || s.RefreshToken == "" {
149 return false, nil
150 }
152 ctx = oidc.ClientContext(ctx, requests.DefaultHTTPClient)
153 err := p.redeemRefreshToken(ctx, s)
154 if err != nil {
155 return false, fmt.Errorf("unable to redeem refresh token: %v", err)
156 }
158 return true, nil
161// redeemRefreshToken uses a RefreshToken with the RedeemURL to refresh the
162// Access Token and (optionally) the ID Token.
163// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
164func (p *OIDCProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) error {
165 clientSecret, err := p.GetClientSecret()
166 if err != nil {
167 return err
168 }
170 c := oauth2.Config{
171 ClientID: p.ClientID,
172 ClientSecret: clientSecret,
173 Endpoint: oauth2.Endpoint{
174 TokenURL: p.RedeemURL.String(),
175 },
176 }
177 t := &oauth2.Token{
178 RefreshToken: s.RefreshToken,
179 Expiry: time.Now().Add(-time.Hour),
180 }
181 token, err := c.TokenSource(ctx, t).Token()
182 if err != nil {
183 return fmt.Errorf("failed to get token: %v", err)
184 }
186 newSession, err := p.createSession(ctx, token, true)
187 if err != nil {
188 return fmt.Errorf("unable create new session state from response: %v", err)
189 }
191 // It's possible that a refresh does not renew the ID Token.
192 // If it doesn't, it's probably better to retain the old one.
193 if newSession.IDToken != "" {
194 s.IDToken = newSession.IDToken
195 s.Email = newSession.Email
196 s.User = newSession.User
197 s.Groups = newSession.Groups
198 s.PreferredUsername = newSession.PreferredUsername
199 }
201 s.AccessToken = newSession.AccessToken
202 s.RefreshToken = newSession.RefreshToken
203 s.CreatedAt = newSession.CreatedAt
204 s.ExpiresOn = newSession.ExpiresOn
205 s.Refreshed = newSession.Refreshed
207 return nil
210// CreateSessionFromToken converts Bearer IDTokens into sessions
211func (p *OIDCProvider) CreateSessionFromToken(ctx context.Context, token string) (*sessions.SessionState, error) {
212 ctx = oidc.ClientContext(ctx, requests.DefaultHTTPClient)
213 idToken, err := p.Verifier.Verify(ctx, token)
214 if err != nil {
215 return nil, err
216 }
218 ss, err := p.buildSessionFromClaims(token, "")
219 if err != nil {
220 return nil, err
221 }
223 // Allow empty Email in Bearer case since we can't hit the ProfileURL
224 if ss.Email == "" {
225 ss.Email = ss.User
226 }
228 ss.AccessToken = token
229 ss.IDToken = token
230 ss.RefreshToken = ""
232 ss.CreatedAtNow()
233 ss.SetExpiresOn(idToken.Expiry)
235 return ss, nil
238// createSession takes an oauth2.Token and creates a SessionState from it.
239// It alters behavior if called from Redeem vs Refresh
240func (p *OIDCProvider) createSession(ctx context.Context, token *oauth2.Token, refresh bool) (*sessions.SessionState, error) {
241 _, err := p.verifyIDToken(ctx, token)
242 if err != nil {
243 switch err {
244 case ErrMissingIDToken:
245 // IDToken is mandatory in Redeem but optional in Refresh
246 if !refresh {
247 return nil, errors.New("token response did not contain an id_token")
248 }
249 default:
250 return nil, fmt.Errorf("could not verify id_token: %v", err)
251 }
252 }
254 rawIDToken := getIDToken(token)
255 ss, err := p.buildSessionFromClaims(rawIDToken, token.AccessToken)
256 if err != nil {
257 return nil, err
258 }
260 ss.AccessToken = token.AccessToken
261 ss.RefreshToken = token.RefreshToken
262 ss.IDToken = rawIDToken
264 ss.CreatedAtNow()
265 ss.SetExpiresOn(token.Expiry)
266 ss.Refreshed = refresh
268 return ss, nil