mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-03 11:22:45 +02:00
Simplified, and de-duplicated many of the configuration settings. Removed configuration settings that could be deduced from other settings. Added some basic documentation. Removed the (duplicate?) user email domain validation check in proxy. Removed the ClientID middleware check. Added a shared key option to be used as a PSK instead of using the IDPs ClientID and ClientSecret. Removed the CookieSecure setting as we only support secure. Added a letsencrypt script to generate a wildcard certificate. Removed the argument in proxy's constructor that allowed arbitrary fucntions to be passed in as validators. Updated proxy's authenticator client to match the server implementation of just using a PSK. Moved debug-mode logging into the log package. Removed unused approval prompt setting. Fixed a bug where identity provider urls were hardcoded. Removed a bunch of unit tests. There have been so many changes many of these tests don't make sense and will need to be re-thought.
254 lines
8 KiB
Go
254 lines
8 KiB
Go
package providers // import "github.com/pomerium/pomerium/internal/providers"
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"time"
|
|
|
|
oidc "github.com/pomerium/go-oidc"
|
|
"github.com/pomerium/pomerium/internal/log"
|
|
"github.com/pomerium/pomerium/internal/sessions"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
const (
|
|
// GoogleProviderName identifies the Google provider
|
|
GoogleProviderName = "google"
|
|
// OIDCProviderName identifes a generic OpenID connect provider
|
|
OIDCProviderName = "oidc"
|
|
// OktaProviderName identifes the Okta identity provider
|
|
OktaProviderName = "okta"
|
|
)
|
|
|
|
// Provider is an interface exposing functions necessary to authenticate with a given provider.
|
|
type Provider interface {
|
|
Data() *ProviderData
|
|
Redeem(string) (*sessions.SessionState, error)
|
|
ValidateSessionState(*sessions.SessionState) bool
|
|
GetSignInURL(state string) string
|
|
RefreshSessionIfNeeded(*sessions.SessionState) (bool, error)
|
|
Revoke(*sessions.SessionState) error
|
|
RefreshAccessToken(string) (string, time.Duration, error)
|
|
// Stop()
|
|
}
|
|
|
|
// New returns a new identity provider based on available name.
|
|
// Defaults to google.
|
|
func New(provider string, p *ProviderData) (Provider, error) {
|
|
switch provider {
|
|
case OIDCProviderName:
|
|
p, err := NewOIDCProvider(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
case OktaProviderName:
|
|
p, err := NewOktaProvider(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
default:
|
|
p, err := NewGoogleProvider(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
}
|
|
}
|
|
|
|
// ProviderData holds the fields associated with providers
|
|
// necessary to implement the Provider interface.
|
|
type ProviderData struct {
|
|
RedirectURL *url.URL
|
|
ProviderName string
|
|
ClientID string
|
|
ClientSecret string
|
|
ProviderURL string
|
|
Scopes []string
|
|
SessionLifetimeTTL time.Duration
|
|
|
|
verifier *oidc.IDTokenVerifier
|
|
oauth *oauth2.Config
|
|
}
|
|
|
|
// Data returns a ProviderData.
|
|
func (p *ProviderData) Data() *ProviderData { return p }
|
|
|
|
// GetSignInURL returns the sign in url with typical oauth parameters
|
|
func (p *ProviderData) GetSignInURL(state string) string {
|
|
return p.oauth.AuthCodeURL(state)
|
|
}
|
|
|
|
// ValidateSessionState validates a given session's from it's JWT token
|
|
// The function verifies it's been signed by the provider, preforms
|
|
// any additional checks depending on the Config, and returns the payload.
|
|
//
|
|
// ValidateSessionState does NOT do nonce validation.
|
|
func (p *ProviderData) ValidateSessionState(s *sessions.SessionState) bool {
|
|
ctx := context.Background()
|
|
_, err := p.verifier.Verify(ctx, s.IDToken)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("authenticate/providers.ValidateSessionState : failed to verify session state")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Redeem creates a session with an identity provider from a authorization code
|
|
func (p *ProviderData) Redeem(code string) (*sessions.SessionState, error) {
|
|
ctx := context.Background()
|
|
// convert authorization code into a token
|
|
token, err := p.oauth.Exchange(ctx, code)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("authenticate/providers.Redeem : token exchange failed")
|
|
return nil, fmt.Errorf("token exchange: %v", err)
|
|
}
|
|
s, err := p.createSessionState(ctx, token)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("authenticate/providers.Redeem : unable to update session")
|
|
return nil, fmt.Errorf("unable to update session: %v", err)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// RefreshSessionIfNeeded will refresh the session state if it's deadline is expired
|
|
func (p *ProviderData) RefreshSessionIfNeeded(s *sessions.SessionState) (bool, error) {
|
|
if !sessionRefreshRequired(s) {
|
|
log.Info().Msg("authenticate/providers.RefreshSessionIfNeeded : session refresh not needed")
|
|
return false, nil
|
|
}
|
|
origExpiration := s.RefreshDeadline
|
|
err := p.redeemRefreshToken(s)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("authenticate/providers.RefreshSession")
|
|
return false, fmt.Errorf("unable to redeem refresh token: %v", err)
|
|
}
|
|
|
|
log.Info().Msgf("authenticate/providers.Redeem refreshed id token %s (expired on %s)", s, origExpiration)
|
|
return true, nil
|
|
}
|
|
|
|
func (p *ProviderData) redeemRefreshToken(s *sessions.SessionState) error {
|
|
log.Info().Msg("authenticate/providers.oidc.redeemRefreshToken 1")
|
|
ctx := context.Background()
|
|
t := &oauth2.Token{
|
|
RefreshToken: s.RefreshToken,
|
|
Expiry: time.Now().Add(-time.Hour),
|
|
}
|
|
log.Info().Msg("authenticate/providers.oidc.redeemRefreshToken 3")
|
|
|
|
// returns a TokenSource automatically refreshing it as necessary using the provided context
|
|
token, err := p.oauth.TokenSource(ctx, t).Token()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("authenticate/providers failed to get token")
|
|
return fmt.Errorf("failed to get token: %v", err)
|
|
}
|
|
log.Info().Msg("authenticate/providers.oidc.redeemRefreshToken 4")
|
|
|
|
newSession, err := p.createSessionState(ctx, token)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("authenticate/providers unable to update session")
|
|
return fmt.Errorf("unable to update session: %v", err)
|
|
}
|
|
s.AccessToken = newSession.AccessToken
|
|
s.IDToken = newSession.IDToken
|
|
s.RefreshToken = newSession.RefreshToken
|
|
s.RefreshDeadline = newSession.RefreshDeadline
|
|
s.Email = newSession.Email
|
|
|
|
log.Info().
|
|
Str("AccessToken", s.AccessToken).
|
|
Str("IdToken", s.IDToken).
|
|
Time("RefreshDeadline", s.RefreshDeadline).
|
|
Str("RefreshToken", s.RefreshToken).
|
|
Str("Email", s.Email).
|
|
Msg("authenticate/providers.redeemRefreshToken")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *ProviderData) 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")
|
|
}
|
|
log.Info().
|
|
Bool("ctx", ctx == nil).
|
|
Bool("Verifier", p.verifier == nil).
|
|
Str("rawIDToken", rawIDToken).
|
|
Msg("authenticate/providers.oidc.createSessionState 2")
|
|
|
|
// Parse and verify ID Token payload.
|
|
idToken, err := p.verifier.Verify(ctx, rawIDToken)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("authenticate/providers could not verify id_token")
|
|
return nil, fmt.Errorf("could not verify id_token: %v", err)
|
|
}
|
|
|
|
// Extract custom claims.
|
|
var claims struct {
|
|
Email string `json:"email"`
|
|
Verified *bool `json:"email_verified"`
|
|
}
|
|
// parse claims from the raw, encoded jwt token
|
|
if err := idToken.Claims(&claims); err != nil {
|
|
return nil, fmt.Errorf("failed to parse id_token claims: %v", err)
|
|
}
|
|
|
|
if claims.Email == "" {
|
|
return nil, fmt.Errorf("id_token did not contain an email")
|
|
}
|
|
if 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,
|
|
RefreshDeadline: token.Expiry,
|
|
LifetimeDeadline: token.Expiry,
|
|
Email: claims.Email,
|
|
}, nil
|
|
}
|
|
|
|
// RefreshAccessToken allows the service to refresh an access token without
|
|
// prompting the user for permission.
|
|
func (p *ProviderData) RefreshAccessToken(refreshToken string) (string, time.Duration, error) {
|
|
if refreshToken == "" {
|
|
return "", 0, errors.New("missing refresh token")
|
|
}
|
|
ctx := context.Background()
|
|
c := oauth2.Config{
|
|
ClientID: p.ClientID,
|
|
ClientSecret: p.ClientSecret,
|
|
Endpoint: oauth2.Endpoint{TokenURL: p.ProviderURL},
|
|
}
|
|
t := oauth2.Token{RefreshToken: refreshToken}
|
|
ts := c.TokenSource(ctx, &t)
|
|
log.Info().
|
|
Str("RefreshToken", refreshToken).
|
|
Msg("authenticate/providers.RefreshAccessToken")
|
|
|
|
newToken, err := ts.Token()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("authenticate/providers.RefreshAccessToken")
|
|
return "", 0, err
|
|
}
|
|
return newToken.AccessToken, newToken.Expiry.Sub(time.Now()), nil
|
|
}
|
|
|
|
// Revoke enables a user to revoke her tokenn. Though many providers such as
|
|
// google and okta provide revoke endpoints, since it's not officially supported
|
|
// as part of OpenID Connect, the default implementation throws an error.
|
|
func (p *ProviderData) Revoke(s *sessions.SessionState) error {
|
|
return errors.New("revoke not implemented")
|
|
}
|
|
|
|
func sessionRefreshRequired(s *sessions.SessionState) bool {
|
|
return s == nil || s.RefreshDeadline.After(time.Now()) || s.RefreshToken == ""
|
|
|
|
}
|