authenticate/providers : add gitlab support (#28)

- Add UserInfo struct and implementation to gather additional
  user information if the endpoint exists.
- Add example docker-compose.yml for on-prem gitlab.
- Add gitlab docs.
- Removed explicit email checks in handlers.
- Providers are now a protected type on provider data.
- Alphabetized provider list.
- Refactored authenticate.New to be more concise.
This commit is contained in:
Bobby DeSimone 2019-01-24 15:10:16 -08:00 committed by GitHub
parent 426e003b03
commit b9c298d278
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 510 additions and 182 deletions

View file

@ -29,7 +29,7 @@ var defaultOptions = &Options{
// Options permits the configuration of the authentication service
type Options struct {
RedirectURL *url.URL `envconfig:"REDIRECT_URL" ` // e.g. auth.example.com/oauth/callback
RedirectURL *url.URL `envconfig:"REDIRECT_URL"`
SharedKey string `envconfig:"SHARED_SECRET"`
@ -49,10 +49,14 @@ type Options struct {
SessionLifetimeTTL time.Duration `envconfig:"SESSION_LIFETIME_TTL"`
// Authentication provider configuration vars
ClientID string `envconfig:"IDP_CLIENT_ID"` // IdP ClientID
ClientSecret string `envconfig:"IDP_CLIENT_SECRET"` // IdP Secret
Provider string `envconfig:"IDP_PROVIDER"` //Provider name e.g. "oidc","okta","google",etc
ProviderURL string `envconfig:"IDP_PROVIDER_URL"`
// See: https://openid.net/specs/openid-connect-basic-1_0.html#RFC6749
ClientID string `envconfig:"IDP_CLIENT_ID"`
ClientSecret string `envconfig:"IDP_CLIENT_SECRET"`
Provider string `envconfig:"IDP_PROVIDER"`
ProviderURL string `envconfig:"IDP_PROVIDER_URL"`
// Scopes is an optional setting corresponding to OAuth 2.0 specification's access scopes
// issuing an Access Token. Named providers are already set with good defaults.
// Most likely only overrides if using the generic OIDC provider.
Scopes []string `envconfig:"IDP_SCOPE"`
SkipProviderButton bool `envconfig:"SKIP_PROVIDER_BUTTON"`
}

View file

@ -197,7 +197,7 @@ func (p *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) {
p.SignInPage(w, r)
}
case sessions.ErrLifetimeExpired, sessions.ErrInvalidSession:
log.Error().Err(err).Msg("authenticate.SignIn : invalid cookie cookie")
log.Error().Err(err).Msg("authenticate.SignIn")
p.sessionStore.ClearSession(w, r)
if p.skipProviderButton {
p.skipButtonOAuthStart(w, r)
@ -394,9 +394,9 @@ func (p *Authenticate) redeemCode(host, code string) (*sessions.SessionState, er
if err != nil {
return nil, err
}
if session.Email == "" {
return nil, fmt.Errorf("no email included in session")
}
// if session.Email == "" {
// return nil, fmt.Errorf("no email included in session")
// }
return session, nil
@ -459,7 +459,7 @@ func (p *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request)
log.Ctx(r.Context()).Info().Str("email", session.Email).Msg("authentication complete")
err = p.sessionStore.SaveSession(w, r, session)
if err != nil {
log.Ctx(r.Context()).Error().Err(err).Msg("internal error")
log.Error().Err(err).Msg("internal error")
return "", httputil.HTTPError{Code: http.StatusInternalServerError, Message: "Internal Error"}
}
return redirect, nil
@ -476,6 +476,7 @@ func (p *Authenticate) OAuthCallback(w http.ResponseWriter, r *http.Request) {
httputil.ErrorResponse(w, r, h.Message, h.Code)
return
default:
log.Error().Err(err).Msg("authenticate.OAuthCallback")
httputil.ErrorResponse(w, r, "Internal Error", http.StatusInternalServerError)
return
}

View file

@ -0,0 +1,79 @@
package providers // import "github.com/pomerium/pomerium/internal/providers"
import (
"context"
"time"
oidc "github.com/pomerium/go-oidc"
"golang.org/x/oauth2"
"github.com/pomerium/pomerium/authenticate/circuit"
"github.com/pomerium/pomerium/internal/log"
)
const defaultGitlabProviderURL = "https://gitlab.com"
// GitlabProvider is an implementation of the Provider interface.
type GitlabProvider struct {
*ProviderData
cb *circuit.Breaker
}
// NewGitlabProvider returns a new Gitlab identity provider; defaults to the hosted version.
//
// Unlike other providers, `email` is not returned from the initial OIDC token. To retrieve email,
// a secondary call must be made to the user's info endpoint. Unfortunately, email is not guaranteed
// or even likely to be returned even if the user has it set as their email must be set to public.
// As pomerium is currently very email centric, I would caution using until Gitlab fixes the issue.
//
// See :
// - https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387
// - https://docs.gitlab.com/ee/integration/openid_connect_provider.html
// - https://docs.gitlab.com/ee/integration/oauth_provider.html
// - https://docs.gitlab.com/ee/api/oauth2.html
// - https://gitlab.com/.well-known/openid-configuration
func NewGitlabProvider(p *ProviderData) (*GitlabProvider, error) {
ctx := context.Background()
if p.ProviderURL == "" {
p.ProviderURL = defaultGitlabProviderURL
}
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
if err != nil {
return nil, err
}
p.Scopes = []string{oidc.ScopeOpenID, "read_user"}
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(),
Scopes: p.Scopes,
}
gitlabProvider := &GitlabProvider{
ProviderData: p,
}
gitlabProvider.cb = circuit.NewBreaker(&circuit.Options{
HalfOpenConcurrentRequests: 2,
OnStateChange: gitlabProvider.cbStateChange,
OnBackoff: gitlabProvider.cbBackoff,
ShouldTripFunc: func(c circuit.Counts) bool { return c.ConsecutiveFailures >= 3 },
ShouldResetFunc: func(c circuit.Counts) bool { return c.ConsecutiveSuccesses >= 6 },
BackoffDurationFunc: circuit.ExponentialBackoffDuration(
time.Duration(200)*time.Second,
time.Duration(500)*time.Millisecond),
})
return gitlabProvider, nil
}
func (p *GitlabProvider) cbBackoff(duration time.Duration, reset time.Time) {
log.Info().Dur("duration", duration).Msg("authenticate/providers/gitlab.cbBackoff")
}
func (p *GitlabProvider) cbStateChange(from, to circuit.State) {
log.Info().Str("from", from.String()).Str("to", to.String()).Msg("authenticate/providers/gitlab.cbStateChange")
}

View file

@ -32,16 +32,17 @@ func NewGoogleProvider(p *ProviderData) (*GoogleProvider, error) {
if p.ProviderURL == "" {
p.ProviderURL = defaultGoogleProviderURL
}
provider, err := oidc.NewProvider(ctx, p.ProviderURL)
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
if err != nil {
return nil, err
}
p.verifier = provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: provider.Endpoint(),
Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
@ -49,12 +50,12 @@ func NewGoogleProvider(p *ProviderData) (*GoogleProvider, error) {
googleProvider := &GoogleProvider{
ProviderData: p,
}
// google supports a revokation endpoint
// google supports a revocation endpoint
var claims struct {
RevokeURL string `json:"revocation_endpoint"`
}
if err := provider.Claims(&claims); err != nil {
if err := p.provider.Claims(&claims); err != nil {
return nil, err
}

View file

@ -38,16 +38,17 @@ func NewAzureProvider(p *ProviderData) (*AzureProvider, error) {
p.ProviderURL = defaultAzureProviderURL
}
log.Info().Msgf("provider url %s", p.ProviderURL)
provider, err := oidc.NewProvider(ctx, p.ProviderURL)
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
if err != nil {
return nil, err
}
p.verifier = provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: provider.Endpoint(),
Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
@ -60,7 +61,7 @@ func NewAzureProvider(p *ProviderData) (*AzureProvider, error) {
RevokeURL string `json:"end_session_endpoint"`
}
if err := provider.Claims(&claims); err != nil {
if err := p.provider.Claims(&claims); err != nil {
return nil, err
}

View file

@ -10,6 +10,7 @@ import (
// OIDCProvider provides a standard, OpenID Connect implementation
// of an authorization identity provider.
// see : https://openid.net/specs/openid-connect-core-1_0.html
type OIDCProvider struct {
*ProviderData
}
@ -20,15 +21,16 @@ func NewOIDCProvider(p *ProviderData) (*OIDCProvider, error) {
if p.ProviderURL == "" {
return nil, errors.New("missing required provider url")
}
provider, err := oidc.NewProvider(ctx, p.ProviderURL)
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
if err != nil {
return nil, err
}
p.verifier = provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: provider.Endpoint(),
Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}

View file

@ -28,26 +28,27 @@ func NewOktaProvider(p *ProviderData) (*OktaProvider, error) {
if p.ProviderURL == "" {
return nil, errors.New("missing required provider url")
}
provider, err := oidc.NewProvider(ctx, p.ProviderURL)
var err error
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
if err != nil {
return nil, err
}
p.verifier = provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.verifier = p.provider.Verifier(&oidc.Config{ClientID: p.ClientID})
p.oauth = &oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: provider.Endpoint(),
Endpoint: p.provider.Endpoint(),
RedirectURL: p.RedirectURL.String(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
oktaProvider := OktaProvider{ProviderData: p}
// okta supports a revokation endpoint
// okta supports a revocation endpoint
var claims struct {
RevokeURL string `json:"revocation_endpoint"`
}
if err := provider.Claims(&claims); err != nil {
if err := p.provider.Claims(&claims); err != nil {
return nil, err
}

View file

@ -2,8 +2,11 @@ package providers // import "github.com/pomerium/pomerium/internal/providers"
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
@ -15,17 +18,19 @@ import (
)
const (
// AzureProviderName identifies the Azure provider
// AzureProviderName identifies the Azure identity provider
AzureProviderName = "azure"
// GoogleProviderName identifies the Google provider
// GitlabProviderName identifies the GitLab identity provider
GitlabProviderName = "gitlab"
// GoogleProviderName identifies the Google identity provider
GoogleProviderName = "google"
// OIDCProviderName identifes a generic OpenID connect provider
// OIDCProviderName identifies a generic OpenID connect provider
OIDCProviderName = "oidc"
// OktaProviderName identifes the Okta identity provider
// OktaProviderName identifies the Okta identity provider
OktaProviderName = "okta"
)
// Provider is an interface exposing functions necessary to authenticate with a given provider.
// Provider is an interface exposing functions necessary to interact with a given provider.
type Provider interface {
Data() *ProviderData
Redeem(string) (*sessions.SessionState, error)
@ -34,38 +39,31 @@ type Provider interface {
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) {
// New returns a new identity provider based given its name.
// Returns an error if selected provided not found or if the provider fails to instantiate.
func New(provider string, pd *ProviderData) (Provider, error) {
var err error
var p Provider
switch provider {
case OIDCProviderName:
p, err := NewOIDCProvider(p)
if err != nil {
return nil, err
}
return p, nil
case AzureProviderName:
p, err := NewAzureProvider(p)
if err != nil {
return nil, err
}
return p, nil
p, err = NewAzureProvider(pd)
case GitlabProviderName:
p, err = NewGitlabProvider(pd)
case GoogleProviderName:
p, err = NewGoogleProvider(pd)
case OIDCProviderName:
p, err = NewOIDCProvider(pd)
case OktaProviderName:
p, err := NewOktaProvider(p)
if err != nil {
return nil, err
}
return p, nil
p, err = NewOktaProvider(pd)
default:
p, err := NewGoogleProvider(p)
if err != nil {
return nil, err
}
return p, nil
return nil, fmt.Errorf("authenticate: provider %q not found", provider)
}
if err != nil {
return nil, err
}
return p, nil
}
// ProviderData holds the fields associated with providers
@ -79,6 +77,7 @@ type ProviderData struct {
Scopes []string
SessionLifetimeTTL time.Duration
provider *oidc.Provider
verifier *oidc.IDTokenVerifier
oauth *oauth2.Config
}
@ -100,7 +99,7 @@ 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")
log.Error().Err(err).Msg("authenticate/providers: failed to verify session state")
return false
}
return true
@ -112,31 +111,47 @@ func (p *ProviderData) Redeem(code string) (*sessions.SessionState, error) {
// 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)
return nil, fmt.Errorf("authenticate/providers: failed 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 nil, fmt.Errorf("authenticate/providers: unable to update session: %v", err)
}
// check if provider has info endpoint, try to hit that and gather more info
// especially useful if initial request did not contain email
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
var claims struct {
UserInfoURL string `json:"userinfo_endpoint"`
}
if err := p.provider.Claims(&claims); err != nil || claims.UserInfoURL == "" {
log.Error().Err(err).Msg("authenticate/providers: failed retrieving userinfo_endpoint")
} else {
// userinfo endpoint found and valid
userInfo, err := p.UserInfo(ctx, claims.UserInfoURL, oauth2.StaticTokenSource(token))
if err != nil {
return nil, fmt.Errorf("authenticate/providers: can't parse userinfo_endpoint: %v", err)
}
s.Email = userInfo.Email
}
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")
log.Debug().Msg("authenticate/providers: 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)
return false, fmt.Errorf("authenticate/providers: couldn't refresh token: %v", err)
}
log.Info().Msgf("authenticate/providers.Redeem refreshed id token %s (expired on %s)", s, origExpiration)
log.Debug().Time("NewDeadline", s.RefreshDeadline).Time("OldDeadline", origExpiration).Msgf("authenticate/providers refreshed")
return true, nil
}
@ -152,15 +167,13 @@ func (p *ProviderData) redeemRefreshToken(s *sessions.SessionState) error {
// 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)
return fmt.Errorf("authenticate/providers: 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)
return fmt.Errorf("authenticate/providers: unable to update session: %v", err)
}
s.AccessToken = newSession.AccessToken
s.IDToken = newSession.IDToken
@ -184,17 +197,11 @@ func (p *ProviderData) createSessionState(ctx context.Context, token *oauth2.Tok
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)
return nil, fmt.Errorf("authenticate/providers: could not verify id_token: %v", err)
}
// Extract custom claims.
@ -204,23 +211,27 @@ func (p *ProviderData) createSessionState(ctx context.Context, token *oauth2.Tok
}
// 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 nil, fmt.Errorf("authenticate/providers: failed to parse id_token claims: %v", err)
}
log.Debug().
Str("AccessToken", token.AccessToken).
Str("IDToken", rawIDToken).
Str("claims.Email", claims.Email).
Str("RefreshToken", token.RefreshToken).
Str("idToken.Subject", idToken.Subject).
Str("idToken.Nonce", idToken.Nonce).
Str("RefreshDeadline", idToken.Expiry.String()).
Str("LifetimeDeadline", idToken.Expiry.String()).
Msg("authenticate/providers.createSessionState")
return &sessions.SessionState{
AccessToken: token.AccessToken,
IDToken: rawIDToken,
RefreshToken: token.RefreshToken,
RefreshDeadline: token.Expiry,
LifetimeDeadline: token.Expiry,
RefreshDeadline: idToken.Expiry,
LifetimeDeadline: idToken.Expiry,
Email: claims.Email,
User: idToken.Subject,
}, nil
}
@ -228,7 +239,7 @@ func (p *ProviderData) createSessionState(ctx context.Context, token *oauth2.Tok
// prompting the user for permission.
func (p *ProviderData) RefreshAccessToken(refreshToken string) (string, time.Duration, error) {
if refreshToken == "" {
return "", 0, errors.New("missing refresh token")
return "", 0, errors.New("authenticate/providers: missing refresh token")
}
ctx := context.Background()
c := oauth2.Config{
@ -250,14 +261,85 @@ func (p *ProviderData) RefreshAccessToken(refreshToken string) (string, time.Dur
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.
// Revoke enables a user to revoke her token. If the identity provider supports revocation
// the endpoint is available, otherwise an error is thrown.
func (p *ProviderData) Revoke(s *sessions.SessionState) error {
return errors.New("revoke not implemented")
return errors.New("authenticate/providers: revoke not implemented")
}
func sessionRefreshRequired(s *sessions.SessionState) bool {
return s == nil || s.RefreshDeadline.After(time.Now()) || s.RefreshToken == ""
}
// UserInfo represents the OpenID Connect userinfo claims.
// see: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
type UserInfo struct {
// Stanard OIDC User fields
Subject string `json:"sub"`
Profile string `json:"profile"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
// custom claims
Name string `json:"name"` // google, gitlab
GivenName string `json:"given_name"` // google
FamilyName string `json:"family_name"` // google
Picture string `json:"picture"` // google,gitlab
Locale string `json:"locale"` // google
Groups []string `json:"groups"` // gitlab
claims []byte
}
// Claims unmarshals the raw JSON object claims into the provided object.
func (u *UserInfo) Claims(v interface{}) error {
if u.claims == nil {
return errors.New("authenticate/providers: claims not set")
}
return json.Unmarshal(u.claims, v)
}
// UserInfo uses the token source to query the provider's user info endpoint.
func (p *ProviderData) UserInfo(ctx context.Context, uri string, tokenSource oauth2.TokenSource) (*UserInfo, error) {
if uri == "" {
return nil, errors.New("authenticate/providers: user info endpoint is not supported by this provider")
}
req, err := http.NewRequest(http.MethodGet, uri, nil)
if err != nil {
return nil, fmt.Errorf("authenticate/providers: create GET request: %v", err)
}
token, err := tokenSource.Token()
if err != nil {
return nil, fmt.Errorf("authenticate/providers: get access token: %v", err)
}
token.SetAuthHeader(req)
resp, err := doRequest(ctx, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: %s", resp.Status, body)
}
var userInfo UserInfo
if err := json.Unmarshal(body, &userInfo); err != nil {
return nil, fmt.Errorf("authenticate/providers failed to decode userinfo: %v", err)
}
userInfo.claims = body
return &userInfo, nil
}
func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
client := http.DefaultClient
if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
client = c
}
return client.Do(req.WithContext(ctx))
}

View file

@ -4,6 +4,7 @@
# NOTE! Generate new SHARED_SECRET and COOKIE_SECRET keys!
# NOTE! Replace `corp.beyondperimeter.com` with whatever your domain is
# NOTE! Make sure certificate files (cert.pem/privkey.pem) are in the same directory as this file
# NOTE! Wrap URLs in quotes to avoid parse errors
version: "3"
services:
# NGINX routes to pomerium's services depending on the request.
@ -28,7 +29,7 @@ services:
- REDIRECT_URL=https://sso-auth.corp.beyondperimeter.com/oauth2/callback
# Identity Provider Settings (Must be changed!)
- IDP_PROVIDER="google"
- IDP_PROVIDER_URL=https://accounts.google.com
- IDP_PROVIDER_URL="https://accounts.google.com"
- IDP_CLIENT_ID=851877082059-bfgkpj09noog7as3gpc3t7r6n9sjbgs6.apps.googleusercontent.com
- IDP_CLIENT_SECRET=P34wwijKRNP3skP5ag5I12kz
- SCOPE="openid email"

View file

@ -0,0 +1,102 @@
version: "3"
services:
# NGINX routes to pomerium's services depending on the request.
nginx:
image: jwilder/nginx-proxy:latest
ports:
- "443:443"
volumes:
# NOTE!!! : nginx must be supplied with your wildcard certificates. And it expects
# it in the format of whatever your wildcard domain name is in.
# see : https://github.com/jwilder/nginx-proxy#wildcard-certificates
# So, if your subdomain is corp.beyondperimeter.com, you'd have the following :
- ./cert.pem:/etc/nginx/certs/corp.beyondperimeter.com.crt:ro
- ./privkey.pem:/etc/nginx/certs/corp.beyondperimeter.com.key:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
pomerium-authenticate:
build: .
restart: always
depends_on:
- "gitlab"
environment:
- POMERIUM_DEBUG=true
- SERVICES=authenticate
# auth settings
- REDIRECT_URL=https://sso-auth.corp.beyondperimeter.com/oauth2/callback
- IDP_PROVIDER="gitlab"
- IDP_PROVIDER_URL=https://gitlab.corp.beyondperimeter.com
- IDP_CLIENT_ID=022dbbd09402441dc7af1924b679bc5e6f5bf0d7a555e55b38c51e2e4e6cee76
- IDP_CLIENT_SECRET=fb7598c520c346915ee369eee57688938fe4f31329a308c4669074da562714b2
- PROXY_ROOT_DOMAIN=beyondperimeter.com
- ALLOWED_DOMAINS=*
- SKIP_PROVIDER_BUTTON=false
# shared service settings
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=sso-auth.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
expose:
- 443
pomerium-proxy:
build: .
restart: always
environment:
- POMERIUM_DEBUG=true
- SERVICES=proxy
# proxy settings
- AUTHENTICATE_SERVICE_URL=https://sso-auth.corp.beyondperimeter.com
- ROUTES=https://httpbin.corp.beyondperimeter.com=http://httpbin,https://hello.corp.beyondperimeter.com=http://hello-world/
# Generate 256 bit random keys e.g. `head -c32 /dev/urandom | base64`
- SHARED_SECRET=aDducXQzK2tPY3R4TmdqTGhaYS80eGYxcTUvWWJDb2M=
- COOKIE_SECRET=V2JBZk0zWGtsL29UcFUvWjVDWWQ2UHExNXJ0b2VhcDI=
- SIGNING_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU0zbXBaSVdYQ1g5eUVneFU2czU3Q2J0YlVOREJTQ0VBdFFGNWZVV0hwY1FvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaFBRditMQUNQVk5tQlRLMHhTVHpicEVQa1JyazFlVXQxQk9hMzJTRWZVUHpOaTRJV2VaLwpLS0lUdDJxMUlxcFYyS01TYlZEeXI5aWp2L1hoOThpeUV3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
# nginx settings
- VIRTUAL_PROTO=https
- VIRTUAL_HOST=*.corp.beyondperimeter.com
- VIRTUAL_PORT=443
volumes: # volumes is optional; used if passing certificates as files
- ./cert.pem:/pomerium/cert.pem:ro
- ./privkey.pem:/pomerium/privkey.pem:ro
expose:
- 443
# https://httpbin.corp.beyondperimeter.com
httpbin:
image: kennethreitz/httpbin:latest
expose:
- 80
# https://hello.corp.beyondperimeter.com
hello-world:
image: tutum/hello-world:latest
expose:
- 80
gitlab:
hostname: gitlab.corp.beyondperimeter.com
image: gitlab/gitlab-ce:latest
restart: always
expose:
- 443
- 80
- 22
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'https://gitlab.corp.beyondperimeter.com'
nginx['ssl_certificate'] = '/etc/gitlab/trusted-certs/corp.beyondperimeter.com.crt'
nginx['ssl_certificate_key'] = '/etc/gitlab/trusted-certs/corp.beyondperimeter.com.key'
VIRTUAL_PROTO: https
VIRTUAL_HOST: gitlab.corp.beyondperimeter.com
VIRTUAL_PORT: 443
volumes:
- ./cert.pem:/etc/gitlab/trusted-certs/corp.beyondperimeter.com.crt
- ./privkey.pem:/etc/gitlab/trusted-certs/corp.beyondperimeter.com.key
- $HOME/gitlab/config:/etc/gitlab
- $HOME/gitlab/logs:/var/log/gitlab
- $HOME/gitlab/data:/var/opt/gitlab

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

View file

@ -1,6 +1,9 @@
---
title: Identity Providers
description: This article describes how to connect pomerium to third-party identity providers / single-sign-on services. You will need to generate keys, copy these into your promerium settings, and enable the connection.
description: >-
This article describes how to connect pomerium to third-party identity
providers / single-sign-on services. You will need to generate keys, copy
these into your promerium settings, and enable the connection.
---
# Identity Provider Configuration
@ -10,89 +13,19 @@ This article describes how to configure pomerium to use a third-party identity s
There are a few configuration steps required for identity provider integration. Most providers support [OpenID Connect] which provides a standardized interface for authentication. In this guide we'll cover how to do the following for each identity provider:
1. Establish a **Redirect URL** with the identity provider which is called after authentication.
1. Generate a **Client ID** and **Client Secret**.
1. Configure pomerium to use the **Client ID** and **Client Secret** keys.
## Google
Log in to your Google account and go to the [APIs & services](https://console.developers.google.com/projectselector/apis/credentials). Navigate to **Credentials** using the left-hand menu.
![API Manager Credentials](./google/google-credentials.png)
On the **Credentials** page, click **Create credentials** and choose **OAuth Client ID**.
![Create New Credentials](./google/google-create-new-credentials.png)
On the **Create Client ID** page, select **Web application**. In the new fields that display, set the following parameters:
| Field | Description |
| ------------------------ | ----------------------------------------- |
| Name | The name of your web app |
| Authorized redirect URIs | `https://${redirect-url}/oauth2/callback` |
![Web App Credentials Configuration](./google/google-create-client-id-config.png)
Click **Create** to proceed.
Your `Client ID` and `Client Secret` will be displayed:
![OAuth Client ID and Secret](./google/google-oauth-client-info.png)
Set `Client ID` and `Client Secret` in Pomerium's settings. Your [environmental variables] should look something like this.
```bash
REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback"
IDP_PROVIDER="google"
IDP_PROVIDER_URL="https://accounts.google.com"
IDP_CLIENT_ID="yyyy.apps.googleusercontent.com"
IDP_CLIENT_SECRET="xxxxxx"
```
## Okta
[Log in to your Okta account](https://login.okta.com) and head to your Okta dashboard. Select **Applications** on the top menu. On the Applications page, click the **Add Application** button to create a new app.
![Okta Applications Dashboard](./okta/okta-app-dashboard.png)
On the **Create New Application** page, select the **Web** for your application.
![Okta Create Application Select Platform](./okta/okta-create-app-platform.png)
Next, provide the following information for your application settings:
| Field | Description |
| ---------------------------- | ----------------------------------------------------- |
| Name | The name of your application. |
| Base URIs (optional) | The domain(s) of your application. |
| Login redirect URIs | `https://${redirect-url}/oauth2/callback`. |
| Group assignments (optional) | The user groups that can sign in to this application. |
| Grant type allowed | **You must enable Refresh Token.** |
![Okta Create Application Settings](./okta/okta-create-app-settings.png)
Click **Done** to proceed. You'll be taken to the **General** page of your app.
Go to the **General** page of your app and scroll down to the **Client Credentials** section. This section contains the **Client ID** and **Client Secret** to be used in the next step.
![Okta Client ID and Secret](./okta/okta-client-id-and-secret.png)
At this point, you will configure the integration from the Pomerium side. Your [environmental variables] should look something like this.
```bash
REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback"
IDP_PROVIDER="okta"
IDP_PROVIDER_URL="https://dev-108295-admin.oktapreview.com/"
IDP_CLIENT_ID="0oairksnr0C0fEJ7l0h7"
IDP_CLIENT_SECRET="xxxxxx"
```
2. Generate a **Client ID** and **Client Secret**.
3. Configure pomerium to use the **Client ID** and **Client Secret** keys.
## Azure
If you plan on allowing users to log in using a Microsoft Azure Active Directory account, either from your company or from external directories, you must register your application through the Microsoft Azure portal. If you don't have a Microsoft Azure account, you can [signup](https://azure.microsoft.com/en-us/free) for free.
You can access the Azure management portal from your Microsoft service, or visit [https://portal.azure.com](https://portal.azure.com) and sign in to Azure using the global administrator account used to create the Office 365 organization.
You can access the Azure management portal from your Microsoft service, or visit <https://portal.azure.com> and sign in to Azure using the global administrator account used to create the Office 365 organization.
::: tip
There is no way to create an application that integrates with Microsoft Azure AD without having **your own** Microsoft Azure AD instance.
:::
If you have an Office 365 account, you can use the account's Azure AD instance instead of creating a new one. To find your Office 365 account's Azure AD instance:
@ -125,21 +58,20 @@ Next you will need to create a key which will be used as the **Client Secret** i
Enter a name for the key and choose the desired duration.
::: tip
If you choose an expiring key, make sure to record the expiration date in your calendar, as you will need to renew the key (get a new one) before that day in order to ensure users don't experience a service interruption.
:::
Click on **Save** and the key will be displayed. **Make sure to copy the value of this key before leaving this screen**, otherwise you may need to create a new key. This value is used as the **Client Secret**.
![Creating a Key](./microsoft/azure-create-key.png)
Next you need to ensure that the Pomerium's Redirect URL is listed in allowed reply URLs for the created application. Navigate to **Azure Active Directory** -> **Apps registrations** and select your app. Then click **Settings** -> **Reply URLs** and add Pomerium's redirect URL. For example,
`https://sso-auth.corp.beyondperimeter.com/oauth2/callback`.
Next you need to ensure that the Pomerium's Redirect URL is listed in allowed reply URLs for the created application. Navigate to **Azure Active Directory** -> **Apps registrations** and select your app. Then click **Settings** -> **Reply URLs** and add Pomerium's redirect URL. For example, `https://sso-auth.corp.beyondperimeter.com/oauth2/callback`.
![Add Reply URL](./microsoft/azure-redirect-url.png)
The final, and most unique step to Azure AD provider, is to take note of your specific endpoint. Navigate to **Azure Active Directory** -> **Apps registrations** and select your app.
![Application dashboard](./microsoft/azure-application-dashbaord.png)
Click on **Endpoints**
The final, and most unique step to Azure AD provider, is to take note of your specific endpoint. Navigate to **Azure Active Directory** -> **Apps registrations** and select your app. ![Application dashboard](./microsoft/azure-application-dashbaord.png) Click on **Endpoints**
![Endpoint details](./microsoft/azure-endpoints.png)
@ -156,7 +88,123 @@ IDP_PROVIDER="azure"
IDP_PROVIDER_URL="https://login.microsoftonline.com/{REPLACE-ME-SEE-ABOVE}/v2.0"
IDP_CLIENT_ID="REPLACE-ME"
IDP_CLIENT_SECRET="REPLACE-ME"
```
## Gitlab
:::warning
Gitlab currently does not provide callers with a user email, under any scope, to a caller unless that user has selected her email to be public. Because Pomerium is by nature very centric, users are cautioned from using Pomerium until [this gitlab bug](https://gitlab.com/gitlab-org/gitlab-ce/issues/44435#note_88150387) is fixed.
:::
Log in to your Gitlab account and go to the [APIs & services](https://console.developers.google.com/projectselector/apis/credentials).
Navigate to **User Settings** then **Applications** using the left-hand menu.
On the **Applications** page, add a new application by setting the following parameters:
Field | Description
------------ | --------------------------------------------
Name | The name of your web app
Redirect URI | `https://${redirect-url}/oauth2/callback`
Scopes | **Must** select **read_user** and **openid**
![Create New Credentials](./gitlab/gitlab-create-application.png)
1.Click **Save Application** to proceed.
Your `Client ID` and `Client Secret` will be displayed:
![Gitlab OAuth Client ID and Secret](./gitlab/gitlab-credentials.png)
Set `Client ID` and `Client Secret` in Pomerium's settings. Your [environmental variables] should look something like this.
```bash
REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback"
IDP_PROVIDER="gitlab"
# NOTE!!! Provider url is optional, but should be set if you are running an on-premise instance
# defaults to : https://gitlab.com, a local copy would look something like `http://gitlab.corp.beyondperimeter.com`
IDP_PROVIDER_URL="https://gitlab.com"
IDP_CLIENT_ID="yyyy"
IDP_CLIENT_SECRET="xxxxxx"
```
When a user first uses pomerium to login, they will be presented with an authorization screen similar to the following.
![gitlab access authorization screen](./gitlab/gitlab-verify-access.png)
## Google
Log in to your Google account and go to the [APIs & services](https://console.developers.google.com/projectselector/apis/credentials). Navigate to **Credentials** using the left-hand menu.
![API Manager Credentials](./google/google-credentials.png)
On the **Credentials** page, click **Create credentials** and choose **OAuth Client ID**.
![Create New Credentials](./google/google-create-new-credentials.png)
On the **Create Client ID** page, select **Web application**. In the new fields that display, set the following parameters:
Field | Description
------------------------ | -----------------------------------------
Name | The name of your web app
Authorized redirect URIs | `https://${redirect-url}/oauth2/callback`
![Web App Credentials Configuration](./google/google-create-client-id-config.png)
Click **Create** to proceed.
Your `Client ID` and `Client Secret` will be displayed:
![OAuth Client ID and Secret](./google/google-oauth-client-info.png)
Set `Client ID` and `Client Secret` in Pomerium's settings. Your [environmental variables] should look something like this.
```bash
REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback"
IDP_PROVIDER="google"
IDP_PROVIDER_URL="https://accounts.google.com"
IDP_CLIENT_ID="yyyy.apps.googleusercontent.com"
IDP_CLIENT_SECRET="xxxxxx"
```
## Okta
[Log in to your Okta account](https://login.okta.com) and head to your Okta dashboard. Select **Applications** on the top menu. On the Applications page, click the **Add Application** button to create a new app.
![Okta Applications Dashboard](./okta/okta-app-dashboard.png)
On the **Create New Application** page, select the **Web** for your application.
![Okta Create Application Select Platform](./okta/okta-create-app-platform.png)
Next, provide the following information for your application settings:
Field | Description
---------------------------- | -----------------------------------------------------
Name | The name of your application.
Base URIs (optional) | The domain(s) of your application.
Login redirect URIs | `https://${redirect-url}/oauth2/callback`.
Group assignments (optional) | The user groups that can sign in to this application.
Grant type allowed | **You must enable Refresh Token.**
![Okta Create Application Settings](./okta/okta-create-app-settings.png)
Click **Done** to proceed. You'll be taken to the **General** page of your app.
Go to the **General** page of your app and scroll down to the **Client Credentials** section. This section contains the **Client ID** and **Client Secret** to be used in the next step.
![Okta Client ID and Secret](./okta/okta-client-id-and-secret.png)
At this point, you will configure the integration from the Pomerium side. Your [environmental variables] should look something like this.
```bash
REDIRECT_URL="https://sso-auth.corp.beyondperimeter.com/oauth2/callback"
IDP_PROVIDER="okta"
IDP_PROVIDER_URL="https://dev-108295-admin.oktapreview.com/"
IDP_CLIENT_ID="0oairksnr0C0fEJ7l0h7"
IDP_CLIENT_SECRET="xxxxxx"
```
[environmental variables]: https://en.wikipedia.org/wiki/Environment_variable

View file

@ -24,24 +24,30 @@ export COOKIE_SECRET=uPGHo1ujND/k3B9V6yr52Gweq3RRYfFho98jxDG5Br8=
# Identity Provider Settings
# OKTA
# export IDP_PROVIDER="okta
# export IDP_CLIENT_ID="REPLACEME"
# export IDP_CLIENT_SECRET="REPLACEME"
# export IDP_PROVIDER_URL="https://REPLACEME.oktapreview.com/oauth2/default"
# Azure
# export IDP_PROVIDER="azure"
# export IDP_PROVIDER_URL="https://login.microsoftonline.com/REPLACEME/v2.0"
# export IDP_CLIENT_ID="REPLACEME
# export IDP_CLIENT_SECRET="REPLACEME"
# Gitlab
# export IDP_PROVIDER="gitlab"
# export IDP_PROVIDER_URL="https://gitlab.onprem.example.com" # optional, defaults to `https://gitlab.com`
# export IDP_CLIENT_ID="REPLACEME
# export IDP_CLIENT_SECRET="REPLACEME"
## GOOGLE
export IDP_PROVIDER="google"
export IDP_PROVIDER_URL="https://accounts.google.com" # optional for google
export IDP_CLIENT_ID="REPLACE-ME.googleusercontent.com"
export IDP_CLIENT_SECRET="REPLACEME"
# OKTA
# export IDP_PROVIDER="okta
# export IDP_CLIENT_ID="REPLACEME"
# export IDP_CLIENT_SECRET="REPLACEME"
# export IDP_PROVIDER_URL="https://REPLACEME.oktapreview.com/oauth2/default"
# export SCOPE="openid email" # generally, you want the default OIDC scopes
# k/v seperated list of simple routes. If no scheme is set, HTTPS will be used.

View file

@ -11,7 +11,7 @@ import (
)
// ErrInvalidSession is an error for invalid sessions.
var ErrInvalidSession = errors.New("invalid session")
var ErrInvalidSession = errors.New("internal/sessions: invalid session")
// CSRFStore has the functions for setting, getting, and clearing the CSRF cookie
type CSRFStore interface {