mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-01-10 04:18:14 +02:00
2eecf756e4
* Current OIDC implementation asserts that user email check must come from JWT token claims. OIDC specification also allows for source of user email to be fetched from userinfo profile endpoint. http://openid.net/specs/openid-connect-core-1_0.html#UserInfo * First, attempt to retrieve email from JWT token claims. Then fall back to requesting email from userinfo endpoint. * Don't fallback to subject for email https://github.com/bitly/oauth2_proxy/pull/481
182 lines
4.9 KiB
Go
182 lines
4.9 KiB
Go
package providers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
oidc "github.com/coreos/go-oidc"
|
|
"github.com/pusher/oauth2_proxy/pkg/apis/sessions"
|
|
"github.com/pusher/oauth2_proxy/pkg/requests"
|
|
|
|
"golang.org/x/oauth2"
|
|
|
|
)
|
|
|
|
// OIDCProvider represents an OIDC based Identity Provider
|
|
type OIDCProvider struct {
|
|
*ProviderData
|
|
|
|
Verifier *oidc.IDTokenVerifier
|
|
AllowUnverifiedEmail bool
|
|
}
|
|
|
|
// NewOIDCProvider initiates a new OIDCProvider
|
|
func NewOIDCProvider(p *ProviderData) *OIDCProvider {
|
|
p.ProviderName = "OpenID Connect"
|
|
return &OIDCProvider{ProviderData: p}
|
|
}
|
|
|
|
// Redeem exchanges the OAuth2 authentication token for an ID token
|
|
func (p *OIDCProvider) Redeem(redirectURL, code string) (s *sessions.SessionState, err error) {
|
|
ctx := context.Background()
|
|
c := oauth2.Config{
|
|
ClientID: p.ClientID,
|
|
ClientSecret: p.ClientSecret,
|
|
Endpoint: oauth2.Endpoint{
|
|
TokenURL: p.RedeemURL.String(),
|
|
},
|
|
RedirectURL: redirectURL,
|
|
}
|
|
token, err := c.Exchange(ctx, code)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("token exchange: %v", err)
|
|
}
|
|
s, err = p.createSessionState(ctx, token)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to update session: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// RefreshSessionIfNeeded checks if the session has expired and uses the
|
|
// RefreshToken to fetch a new ID token if required
|
|
func (p *OIDCProvider) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
|
|
if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
|
|
return false, nil
|
|
}
|
|
|
|
origExpiration := s.ExpiresOn
|
|
|
|
err := p.redeemRefreshToken(s)
|
|
if err != nil {
|
|
return false, fmt.Errorf("unable to redeem refresh token: %v", err)
|
|
}
|
|
|
|
fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
|
|
return true, nil
|
|
}
|
|
|
|
func (p *OIDCProvider) redeemRefreshToken(s *sessions.SessionState) (err error) {
|
|
c := oauth2.Config{
|
|
ClientID: p.ClientID,
|
|
ClientSecret: p.ClientSecret,
|
|
Endpoint: oauth2.Endpoint{
|
|
TokenURL: p.RedeemURL.String(),
|
|
},
|
|
}
|
|
ctx := context.Background()
|
|
t := &oauth2.Token{
|
|
RefreshToken: s.RefreshToken,
|
|
Expiry: time.Now().Add(-time.Hour),
|
|
}
|
|
token, err := c.TokenSource(ctx, t).Token()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get token: %v", err)
|
|
}
|
|
newSession, err := p.createSessionState(ctx, token)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to update session: %v", err)
|
|
}
|
|
s.AccessToken = newSession.AccessToken
|
|
s.IDToken = newSession.IDToken
|
|
s.RefreshToken = newSession.RefreshToken
|
|
s.CreatedAt = newSession.CreatedAt
|
|
s.ExpiresOn = newSession.ExpiresOn
|
|
s.Email = newSession.Email
|
|
return
|
|
}
|
|
|
|
func (p *OIDCProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
|
|
rawIDToken, ok := token.Extra("id_token").(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("token response did not contain an id_token")
|
|
}
|
|
|
|
// Parse and verify ID Token payload.
|
|
idToken, err := p.Verifier.Verify(ctx, rawIDToken)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not verify id_token: %v", err)
|
|
}
|
|
|
|
// Extract custom claims.
|
|
var claims struct {
|
|
Subject string `json:"sub"`
|
|
Email string `json:"email"`
|
|
Verified *bool `json:"email_verified"`
|
|
}
|
|
if err := idToken.Claims(&claims); err != nil {
|
|
return nil, fmt.Errorf("failed to parse id_token claims: %v", err)
|
|
}
|
|
|
|
if claims.Email == "" {
|
|
if p.ProfileURL.String() == "" {
|
|
return nil, fmt.Errorf("id_token did not contain an email")
|
|
}
|
|
|
|
// If the userinfo endpoint profileURL is defined, then there is a chance the userinfo
|
|
// contents at the profileURL contains the email.
|
|
// Make a query to the userinfo endpoint, and attempt to locate the email from there.
|
|
|
|
req, err := http.NewRequest("GET", p.ProfileURL.String(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header = getOIDCHeader(token.AccessToken)
|
|
|
|
json, err := requests.Request(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
email, err := json.Get("email").String()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("id_token nor userinfo endpoint did not contain an email")
|
|
}
|
|
|
|
claims.Email = email
|
|
}
|
|
if !p.AllowUnverifiedEmail && claims.Verified != nil && !*claims.Verified {
|
|
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
|
|
}
|
|
|
|
return &sessions.SessionState{
|
|
AccessToken: token.AccessToken,
|
|
IDToken: rawIDToken,
|
|
RefreshToken: token.RefreshToken,
|
|
CreatedAt: time.Now(),
|
|
ExpiresOn: idToken.Expiry,
|
|
Email: claims.Email,
|
|
User: claims.Subject,
|
|
}, nil
|
|
}
|
|
|
|
// ValidateSessionState checks that the session's IDToken is still valid
|
|
func (p *OIDCProvider) ValidateSessionState(s *sessions.SessionState) bool {
|
|
ctx := context.Background()
|
|
_, err := p.Verifier.Verify(ctx, s.IDToken)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func getOIDCHeader(access_token string) http.Header {
|
|
header := make(http.Header)
|
|
header.Set("Accept", "application/json")
|
|
header.Set("Authorization", fmt.Sprintf("Bearer %s", access_token))
|
|
return header
|
|
}
|