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) respJson, err := requests.Request(req) if err != nil { return nil, err } email, err := respJson.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(accessToken string) http.Header { header := make(http.Header) header.Set("Accept", "application/json") header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) return header }