mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-28 18:06:34 +02:00
identity: abstract identity providers by type (#560)
Signed-off-by: Bobby DeSimone <bobbydesimone@gmail.com>
This commit is contained in:
parent
f4868dd4dd
commit
627a591824
20 changed files with 773 additions and 746 deletions
|
@ -219,7 +219,10 @@ issues:
|
|||
text: "please use NewService instead"
|
||||
linters:
|
||||
- staticcheck
|
||||
|
||||
- path: internal/identity/oauth/github/github.go
|
||||
text: "Potential hardcoded credentials"
|
||||
linters:
|
||||
- gosec
|
||||
# Independently from option `exclude` we use default exclude patterns,
|
||||
# it can be disabled by this option. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`.
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/pomerium/pomerium/internal/grpc"
|
||||
"github.com/pomerium/pomerium/internal/grpc/cache/client"
|
||||
"github.com/pomerium/pomerium/internal/identity"
|
||||
"github.com/pomerium/pomerium/internal/identity/oauth"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/sessions/cache"
|
||||
"github.com/pomerium/pomerium/internal/sessions/cookie"
|
||||
|
@ -155,9 +156,8 @@ func New(opts config.Options) (*Authenticate, error) {
|
|||
redirectURL, _ := urlutil.DeepCopy(opts.AuthenticateURL)
|
||||
redirectURL.Path = opts.AuthenticateCallbackPath
|
||||
// configure our identity provider
|
||||
provider, err := identity.New(
|
||||
opts.Provider,
|
||||
&identity.Provider{
|
||||
provider, err := identity.NewAuthenticator(
|
||||
oauth.Options{
|
||||
RedirectURL: redirectURL,
|
||||
ProviderName: opts.Provider,
|
||||
ProviderURL: opts.ProviderURL,
|
||||
|
@ -166,6 +166,7 @@ func New(opts config.Options) (*Authenticate, error) {
|
|||
Scopes: opts.Scopes,
|
||||
ServiceAccount: opts.ServiceAccount,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
"github.com/pomerium/csrf"
|
||||
"github.com/pomerium/pomerium/internal/cryptutil"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/identity"
|
||||
"github.com/pomerium/pomerium/internal/identity/oidc"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/middleware"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
|
@ -233,7 +233,7 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
// first, try to revoke the session if implemented
|
||||
err = a.provider.Revoke(r.Context(), s.AccessToken)
|
||||
if err != nil && !errors.Is(err, identity.ErrRevokeNotImplemented) {
|
||||
if err != nil && !errors.Is(err, oidc.ErrRevokeNotImplemented) {
|
||||
return httputil.NewError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
|
@ -244,7 +244,7 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) error {
|
|||
params.Add("post_logout_redirect_uri", redirectString)
|
||||
endSessionURL.RawQuery = params.Encode()
|
||||
redirectString = endSessionURL.String()
|
||||
} else if !errors.Is(err, identity.ErrSignoutNotImplemented) {
|
||||
} else if !errors.Is(err, oidc.ErrSignoutNotImplemented) {
|
||||
return httputil.NewError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/pomerium/pomerium/internal/frontend"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/identity"
|
||||
"github.com/pomerium/pomerium/internal/identity/oidc"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/sessions/cookie"
|
||||
mstore "github.com/pomerium/pomerium/internal/sessions/mock"
|
||||
|
@ -192,7 +193,7 @@ func TestAuthenticate_SignOut(t *testing.T) {
|
|||
{"good post", http.MethodPost, nil, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{LogOutResponse: (*uriParseHelper("https://microsoft.com"))}, &mstore.Store{Encrypted: true, Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusFound, ""},
|
||||
{"failed revoke", http.MethodPost, nil, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &mstore.Store{Encrypted: true, Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: OH NO\"}\n"},
|
||||
{"load session error", http.MethodPost, errors.New("error"), "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{RevokeError: errors.New("OH NO")}, &mstore.Store{Encrypted: true, Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: error\"}\n"},
|
||||
{"bad redirect uri", http.MethodPost, nil, "corp.pomerium.io/", "sig", "ts", identity.MockProvider{LogOutError: identity.ErrSignoutNotImplemented}, &mstore.Store{Encrypted: true, Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: corp.pomerium.io/ url does contain a valid scheme\"}\n"},
|
||||
{"bad redirect uri", http.MethodPost, nil, "corp.pomerium.io/", "sig", "ts", identity.MockProvider{LogOutError: oidc.ErrSignoutNotImplemented}, &mstore.Store{Encrypted: true, Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusBadRequest, "{\"Status\":400,\"Error\":\"Bad Request: corp.pomerium.io/ url does contain a valid scheme\"}\n"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
package identity
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrRevokeNotImplemented error type when Revoke method is not implemented
|
||||
// by an identity provider
|
||||
var ErrRevokeNotImplemented = errors.New("internal/identity: revoke not implemented")
|
||||
|
||||
// ErrSignoutNotImplemented error type when end session is not implemented
|
||||
// by an identity provider
|
||||
// https://openid.net/specs/openid-connect-frontchannel-1_0.html#RPInitiated
|
||||
var ErrSignoutNotImplemented = errors.New("internal/identity: end session not implemented")
|
|
@ -1,101 +0,0 @@
|
|||
package identity // import "github.com/pomerium/pomerium/internal/identity"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultGitLabProviderURL = "https://gitlab.com"
|
||||
groupPath = "/api/v4/groups"
|
||||
)
|
||||
|
||||
// GitLabProvider is an implementation of the OAuth Provider
|
||||
type GitLabProvider struct {
|
||||
*Provider
|
||||
}
|
||||
|
||||
// NewGitLabProvider returns a new GitLabProvider.
|
||||
// https://www.pomerium.io/docs/identity-providers/gitlab.html
|
||||
func NewGitLabProvider(p *Provider) (*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
|
||||
}
|
||||
|
||||
if len(p.Scopes) == 0 {
|
||||
p.Scopes = []string{oidc.ScopeOpenID, "api", "read_user", "profile", "email"}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
gp := &GitLabProvider{Provider: p}
|
||||
|
||||
if err := p.provider.Claims(&gp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gp.UserGroupFn = gp.UserGroups
|
||||
return gp, nil
|
||||
}
|
||||
|
||||
// UserGroups returns a slice of groups for the user.
|
||||
//
|
||||
// By default, this request returns 20 results at a time because the API results are paginated.
|
||||
// https://docs.gitlab.com/ee/api/groups.html#list-groups
|
||||
func (p *GitLabProvider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
||||
if s == nil || s.AccessToken == nil {
|
||||
return nil, errors.New("identity/gitlab: user session cannot be empty")
|
||||
}
|
||||
|
||||
var response []struct {
|
||||
ID json.Number `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Visibility string `json:"visibility,omitempty"`
|
||||
ShareWithGroupLock bool `json:"share_with_group_lock,omitempty"`
|
||||
RequireTwoFactorAuthentication bool `json:"require_two_factor_authentication,omitempty"`
|
||||
SubgroupCreationLevel string `json:"subgroup_creation_level,omitempty"`
|
||||
FullName string `json:"full_name,omitempty"`
|
||||
FullPath string `json:"full_path,omitempty"`
|
||||
}
|
||||
userGroupURL := p.ProviderURL + groupPath
|
||||
headers := map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.AccessToken.AccessToken)}
|
||||
err := httputil.Client(ctx, http.MethodGet, userGroupURL, version.UserAgent(), headers, nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var groups []string
|
||||
log.Debug().Interface("response", response).Msg("identity/gitlab: groups")
|
||||
|
||||
for _, group := range response {
|
||||
groups = append(groups, group.ID.String())
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
// defaultAzureProviderURL Users with both a personal Microsoft
|
||||
// account and a work or school account from Azure Active Directory (Azure AD)
|
||||
// an sign in to the application.
|
||||
const defaultAzureProviderURL = "https://login.microsoftonline.com/common"
|
||||
const defaultAzureGroupURL = "https://graph.microsoft.com/v1.0/me/memberOf"
|
||||
|
||||
// AzureProvider is an implementation of the Provider interface
|
||||
type AzureProvider struct {
|
||||
*Provider
|
||||
}
|
||||
|
||||
// NewAzureProvider returns a new AzureProvider and sets the provider url endpoints.
|
||||
// https://www.pomerium.io/docs/identity-providers.html#azure-active-directory
|
||||
func NewAzureProvider(p *Provider) (*AzureProvider, error) {
|
||||
ctx := context.Background()
|
||||
if p.ProviderURL == "" {
|
||||
p.ProviderURL = defaultAzureProviderURL
|
||||
}
|
||||
var err error
|
||||
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(p.Scopes) == 0 {
|
||||
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "offline_access", "Group.Read.All"}
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
azureProvider := &AzureProvider{Provider: p}
|
||||
if err := p.provider.Claims(&azureProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.UserGroupFn = azureProvider.UserGroups
|
||||
|
||||
return azureProvider, nil
|
||||
}
|
||||
|
||||
// GetSignInURL returns the sign in url with typical oauth parameters
|
||||
func (p *AzureProvider) GetSignInURL(state string) string {
|
||||
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "select_account"))
|
||||
}
|
||||
|
||||
// UserGroups returns a slice of group names a given user is in.
|
||||
// `Directory.Read.All` is required.
|
||||
// https://docs.microsoft.com/en-us/graph/api/resources/directoryobject?view=graph-rest-1.0
|
||||
// https://docs.microsoft.com/en-us/graph/api/user-list-memberof?view=graph-rest-1.0
|
||||
func (p *AzureProvider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
||||
if s == nil || s.AccessToken == nil {
|
||||
return nil, errors.New("identity/azure: session cannot be nil")
|
||||
}
|
||||
var response struct {
|
||||
Groups []struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description,omitempty"`
|
||||
DisplayName string `json:"displayName"`
|
||||
CreatedDateTime time.Time `json:"createdDateTime,omitempty"`
|
||||
GroupTypes []string `json:"groupTypes,omitempty"`
|
||||
} `json:"value"`
|
||||
}
|
||||
headers := map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.AccessToken.AccessToken)}
|
||||
err := httputil.Client(ctx, http.MethodGet, defaultAzureGroupURL, version.UserAgent(), headers, nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var groups []string
|
||||
for _, group := range response.Groups {
|
||||
log.Debug().Str("DisplayName", group.DisplayName).Str("ID", group.ID).Msg("identity/microsoft: group")
|
||||
groups = append(groups, group.ID)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
package identity
|
||||
// Package github implements OAuth2 based authentication for github
|
||||
//
|
||||
// https://www.pomerium.io/docs/identity-providers/github.html
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -10,68 +13,75 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/identity/oauth"
|
||||
pom_oidc "github.com/pomerium/pomerium/internal/identity/oidc"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
// Name identifies the GitHub identity provider
|
||||
const Name = "github"
|
||||
|
||||
const (
|
||||
defaultGitHubProviderURL = "https://github.com"
|
||||
githubAPIURL = "https://api.github.com"
|
||||
userPath = "/user"
|
||||
teamPath = "/user/teams"
|
||||
revokePath = "/applications/%s/grant"
|
||||
emailPath = "/user/emails"
|
||||
defaultProviderURL = "https://github.com"
|
||||
githubAPIURL = "https://api.github.com"
|
||||
userPath = "/user"
|
||||
teamPath = "/user/teams"
|
||||
revokePath = "/applications/%s/grant"
|
||||
emailPath = "/user/emails"
|
||||
// https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps
|
||||
authURL = "/login/oauth/authorize"
|
||||
tokenURL = "/login/oauth/access_token"
|
||||
|
||||
// since github doesn't implement oidc, we need this to refresh the user session
|
||||
refreshDeadline = time.Minute * 60
|
||||
)
|
||||
|
||||
// GitHubProvider is an implementation of the OAuth Provider.
|
||||
type GitHubProvider struct {
|
||||
*Provider
|
||||
// https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
|
||||
var defaultScopes = []string{"user:email", "read:org"}
|
||||
|
||||
// Provider is an implementation of the OAuth Provider.
|
||||
type Provider struct {
|
||||
*pom_oidc.Provider
|
||||
|
||||
userEndpoint string
|
||||
}
|
||||
|
||||
// NewGitHubProvider returns a new GitHubProvider.
|
||||
func NewGitHubProvider(p *Provider) (*GitHubProvider, error) {
|
||||
if p.ProviderURL == "" {
|
||||
p.ProviderURL = defaultGitHubProviderURL
|
||||
// New instantiates an OAuth2 provider for Github.
|
||||
func New(ctx context.Context, o *oauth.Options) (*Provider, error) {
|
||||
var p Provider
|
||||
if o.ProviderURL == "" {
|
||||
o.ProviderURL = defaultProviderURL
|
||||
}
|
||||
|
||||
if len(p.Scopes) == 0 {
|
||||
p.Scopes = []string{"user:email", "read:org"}
|
||||
if len(o.Scopes) == 0 {
|
||||
o.Scopes = defaultScopes
|
||||
}
|
||||
|
||||
p.oauth = &oauth2.Config{
|
||||
ClientID: p.ClientID,
|
||||
ClientSecret: p.ClientSecret,
|
||||
p.Oauth = &oauth2.Config{
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: p.ProviderURL + "/login/oauth/authorize",
|
||||
TokenURL: p.ProviderURL + "/login/oauth/access_token",
|
||||
AuthURL: o.ProviderURL + authURL,
|
||||
TokenURL: o.ProviderURL + tokenURL,
|
||||
},
|
||||
RedirectURL: p.RedirectURL.String(),
|
||||
Scopes: p.Scopes,
|
||||
}
|
||||
gp := &GitHubProvider{
|
||||
Provider: p,
|
||||
userEndpoint: githubAPIURL + userPath,
|
||||
RedirectURL: o.RedirectURL.String(),
|
||||
Scopes: o.Scopes,
|
||||
}
|
||||
p.userEndpoint = githubAPIURL + userPath
|
||||
|
||||
return gp, nil
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// Authenticate creates an identity session with github from a authorization code, and follows up
|
||||
// call to the user and user group endpoint with the
|
||||
func (p *GitHubProvider) Authenticate(ctx context.Context, code string) (*sessions.State, error) {
|
||||
resp, err := p.oauth.Exchange(ctx, code)
|
||||
func (p *Provider) Authenticate(ctx context.Context, code string) (*sessions.State, error) {
|
||||
resp, err := p.Oauth.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identity/github: token exchange failed %v", err)
|
||||
return nil, fmt.Errorf("github: token exchange failed %v", err)
|
||||
}
|
||||
|
||||
s := &sessions.State{
|
||||
|
@ -93,34 +103,34 @@ func (p *GitHubProvider) Authenticate(ctx context.Context, code string) (*sessio
|
|||
// updateSessionState will get the user information from github and also retrieve the user's team(s)
|
||||
//
|
||||
// https://developer.github.com/v3/users/#get-the-authenticated-user
|
||||
func (p *GitHubProvider) updateSessionState(ctx context.Context, s *sessions.State) error {
|
||||
func (p *Provider) updateSessionState(ctx context.Context, s *sessions.State) error {
|
||||
if s == nil || s.AccessToken == nil {
|
||||
return errors.New("identity/github: user session cannot be empty")
|
||||
return errors.New("github: user session cannot be empty")
|
||||
}
|
||||
accessToken := s.AccessToken.AccessToken
|
||||
|
||||
err := p.userInfo(ctx, accessToken, s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("identity/github: could not retrieve user info %w", err)
|
||||
return fmt.Errorf("github: could not retrieve user info %w", err)
|
||||
}
|
||||
|
||||
err = p.userEmail(ctx, accessToken, s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("identity/github: could not retrieve user email %w", err)
|
||||
return fmt.Errorf("github: could not retrieve user email %w", err)
|
||||
}
|
||||
|
||||
err = p.userTeams(ctx, accessToken, s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("identity/github: could not retrieve groups %w", err)
|
||||
return fmt.Errorf("github: could not retrieve groups %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh renews a user's session by making a new userInfo request.
|
||||
func (p *GitHubProvider) Refresh(ctx context.Context, s *sessions.State) (*sessions.State, error) {
|
||||
func (p *Provider) Refresh(ctx context.Context, s *sessions.State) (*sessions.State, error) {
|
||||
if s.AccessToken == nil {
|
||||
return nil, errors.New("identity/github: missing oauth2 access token")
|
||||
return nil, errors.New("github: missing oauth2 access token")
|
||||
}
|
||||
if err := p.updateSessionState(ctx, s); err != nil {
|
||||
return nil, err
|
||||
|
@ -133,7 +143,7 @@ func (p *GitHubProvider) Refresh(ctx context.Context, s *sessions.State) (*sessi
|
|||
//
|
||||
// https://developer.github.com/v3/teams/#list-user-teams
|
||||
// https://developer.github.com/v3/auth/
|
||||
func (p *GitHubProvider) userTeams(ctx context.Context, at string, s *sessions.State) error {
|
||||
func (p *Provider) userTeams(ctx context.Context, at string, s *sessions.State) error {
|
||||
|
||||
var response []struct {
|
||||
ID json.Number `json:"id"`
|
||||
|
@ -152,8 +162,7 @@ func (p *GitHubProvider) userTeams(ctx context.Context, at string, s *sessions.S
|
|||
return err
|
||||
}
|
||||
|
||||
log.Debug().Interface("teams", response).Msg("identity/github: user teams")
|
||||
|
||||
log.Debug().Interface("teams", response).Msg("github: user teams")
|
||||
s.Groups = nil
|
||||
for _, org := range response {
|
||||
s.Groups = append(s.Groups, org.ID.String())
|
||||
|
@ -167,7 +176,7 @@ func (p *GitHubProvider) userTeams(ctx context.Context, at string, s *sessions.S
|
|||
//
|
||||
// https://developer.github.com/v3/users/emails/#list-email-addresses-for-a-user
|
||||
// https://developer.github.com/v3/auth/
|
||||
func (p *GitHubProvider) userEmail(ctx context.Context, at string, s *sessions.State) error {
|
||||
func (p *Provider) userEmail(ctx context.Context, at string, s *sessions.State) error {
|
||||
// response represents the github user email
|
||||
// https://developer.github.com/v3/users/emails/#response
|
||||
var response []struct {
|
||||
|
@ -183,7 +192,7 @@ func (p *GitHubProvider) userEmail(ctx context.Context, at string, s *sessions.S
|
|||
return err
|
||||
}
|
||||
|
||||
log.Debug().Interface("emails", response).Msg("identity/github: user emails")
|
||||
log.Debug().Interface("emails", response).Msg("github: user emails")
|
||||
for _, email := range response {
|
||||
if email.Primary && email.Verified {
|
||||
s.Email = email.Email
|
||||
|
@ -194,7 +203,7 @@ func (p *GitHubProvider) userEmail(ctx context.Context, at string, s *sessions.S
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *GitHubProvider) userInfo(ctx context.Context, at string, s *sessions.State) error {
|
||||
func (p *Provider) userInfo(ctx context.Context, at string, s *sessions.State) error {
|
||||
var response struct {
|
||||
ID int `json:"id"`
|
||||
Login string `json:"login"`
|
||||
|
@ -224,19 +233,19 @@ func (p *GitHubProvider) userInfo(ctx context.Context, at string, s *sessions.St
|
|||
// gave pomerium application during authorization.
|
||||
//
|
||||
// https://developer.github.com/v3/apps/oauth_applications/#delete-an-app-authorization
|
||||
func (p *GitHubProvider) Revoke(ctx context.Context, token *oauth2.Token) error {
|
||||
func (p *Provider) Revoke(ctx context.Context, token *oauth2.Token) error {
|
||||
// build the basic authentication request
|
||||
basicAuth := url.UserPassword(p.ClientID, p.ClientSecret)
|
||||
basicAuth := url.UserPassword(p.Oauth.ClientID, p.Oauth.ClientSecret)
|
||||
revokeURL := url.URL{
|
||||
Scheme: "https",
|
||||
User: basicAuth,
|
||||
Host: "api.github.com",
|
||||
Path: fmt.Sprintf(revokePath, p.ClientID),
|
||||
Path: fmt.Sprintf(revokePath, p.Oauth.ClientID),
|
||||
}
|
||||
reqBody := strings.NewReader(fmt.Sprintf(`{"access_token": "%s"}`, token.AccessToken))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, revokeURL.String(), reqBody)
|
||||
if err != nil {
|
||||
return errors.New("identity/github could not create revoke request")
|
||||
return errors.New("github: could not create revoke request")
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
32
internal/identity/oauth/options.go
Normal file
32
internal/identity/oauth/options.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Package oauth provides support for making OAuth2 authorized and authenticated
|
||||
// HTTP requests, as specified in RFC 6749. It can additionally grant
|
||||
// authorization with Bearer JWT.
|
||||
package oauth
|
||||
|
||||
import "net/url"
|
||||
|
||||
// Options contains the fields required for an OAuth 2.0 (inc. OIDC) auth flow.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc6749
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
type Options struct {
|
||||
ProviderName string
|
||||
|
||||
// ProviderURL is the endpoint to look for .well-known/openid-configuration
|
||||
// OAuth2 related endpoints and will be autoconfigured based off this URL
|
||||
ProviderURL string
|
||||
|
||||
// ClientID is the application's ID.
|
||||
ClientID string
|
||||
// ClientSecret is the application's secret.
|
||||
ClientSecret string
|
||||
// RedirectURL is the URL to redirect users going through
|
||||
// the OAuth flow, after the resource owner's URLs.
|
||||
RedirectURL *url.URL
|
||||
// Scope specifies optional requested permissions.
|
||||
Scopes []string
|
||||
|
||||
// ServiceAccount can be set for those providers that require additional
|
||||
// credentials or tokens to do follow up API calls (e.g. Google)
|
||||
ServiceAccount string
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// OIDCProvider provides a standard, OpenID Connect implementation
|
||||
// of an authorization identity provider.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html
|
||||
type OIDCProvider struct {
|
||||
*Provider
|
||||
}
|
||||
|
||||
// NewOIDCProvider creates a new instance of a generic OpenID Connect provider.
|
||||
func NewOIDCProvider(p *Provider) (*OIDCProvider, error) {
|
||||
ctx := context.Background()
|
||||
if p.ProviderURL == "" {
|
||||
return nil, ErrMissingProviderURL
|
||||
}
|
||||
var err error
|
||||
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(p.Scopes) == 0 {
|
||||
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "offline_access"}
|
||||
}
|
||||
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,
|
||||
}
|
||||
return &OIDCProvider{Provider: p}, nil
|
||||
}
|
87
internal/identity/oidc/azure/microsoft.go
Normal file
87
internal/identity/oidc/azure/microsoft.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
// Package azure implements OpenID Connect for Microsoft Azure
|
||||
//
|
||||
// https://www.pomerium.io/docs/identity-providers/azure.html
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/identity/oauth"
|
||||
pom_oidc "github.com/pomerium/pomerium/internal/identity/oidc"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
// Name identifies the Azure identity provider
|
||||
const Name = "azure"
|
||||
|
||||
// defaultProviderURL Users with both a personal Microsoft
|
||||
// account and a work or school account from Azure Active Directory (Azure AD)
|
||||
// an sign in to the application.
|
||||
const defaultProviderURL = "https://login.microsoftonline.com/common"
|
||||
const defaultGroupURL = "https://graph.microsoft.com/v1.0/me/memberOf"
|
||||
|
||||
// Provider is an Azure implementation of the Authenticator interface.
|
||||
type Provider struct {
|
||||
*pom_oidc.Provider
|
||||
}
|
||||
|
||||
// New instantiates an OpenID Connect (OIDC) provider for Azure.
|
||||
func New(ctx context.Context, o *oauth.Options) (*Provider, error) {
|
||||
var p Provider
|
||||
var err error
|
||||
if o.ProviderURL == "" {
|
||||
o.ProviderURL = defaultProviderURL
|
||||
}
|
||||
genericOidc, err := pom_oidc.New(ctx, o)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed creating oidc provider: %w", Name, err)
|
||||
}
|
||||
p.Provider = genericOidc
|
||||
p.UserGroupFn = p.UserGroups
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// GetSignInURL returns the sign in url with typical oauth parameters
|
||||
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow
|
||||
func (p *Provider) GetSignInURL(state string) string {
|
||||
return p.Oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "select_account"))
|
||||
}
|
||||
|
||||
// UserGroups returns a slice of group names a given user is in.
|
||||
// `Directory.Read.All` is required.
|
||||
// https://docs.microsoft.com/en-us/graph/api/resources/directoryobject?view=graph-rest-1.0
|
||||
// https://docs.microsoft.com/en-us/graph/api/user-list-memberof?view=graph-rest-1.0
|
||||
func (p *Provider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
||||
if s == nil || s.AccessToken == nil {
|
||||
return nil, errors.New("identity/azure: session cannot be nil")
|
||||
}
|
||||
var response struct {
|
||||
Groups []struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description,omitempty"`
|
||||
DisplayName string `json:"displayName"`
|
||||
CreatedDateTime time.Time `json:"createdDateTime,omitempty"`
|
||||
GroupTypes []string `json:"groupTypes,omitempty"`
|
||||
} `json:"value"`
|
||||
}
|
||||
headers := map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.AccessToken.AccessToken)}
|
||||
err := httputil.Client(ctx, http.MethodGet, defaultGroupURL, version.UserAgent(), headers, nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var groups []string
|
||||
for _, group := range response.Groups {
|
||||
log.Debug().Str("DisplayName", group.DisplayName).Str("ID", group.ID).Msg("microsoft: group")
|
||||
groups = append(groups, group.ID)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
16
internal/identity/oidc/errors.go
Normal file
16
internal/identity/oidc/errors.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package oidc
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrRevokeNotImplemented error type when Revoke method is not implemented
|
||||
// by an identity provider
|
||||
var ErrRevokeNotImplemented = errors.New("identity/oidc: revoke not implemented")
|
||||
|
||||
// ErrSignoutNotImplemented error type when end session is not implemented
|
||||
// by an identity provider
|
||||
// https://openid.net/specs/openid-connect-frontchannel-1_0.html#RPInitiated
|
||||
var ErrSignoutNotImplemented = errors.New("identity/oidc: end session not implemented")
|
||||
|
||||
// ErrMissingProviderURL is returned when an identity provider requires a provider url
|
||||
// does not receive one.
|
||||
var ErrMissingProviderURL = errors.New("identity/oidc: missing provider url")
|
97
internal/identity/oidc/gitlab/gitlab.go
Normal file
97
internal/identity/oidc/gitlab/gitlab.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
// Package gitlab implements OpenID Connect for Gitlab
|
||||
//
|
||||
// https://www.pomerium.io/docs/identity-providers/gitlab.html
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/identity/oauth"
|
||||
pom_oidc "github.com/pomerium/pomerium/internal/identity/oidc"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
// Name identifies the GitLab identity provider
|
||||
const Name = "gitlab"
|
||||
|
||||
var defaultScopes = []string{oidc.ScopeOpenID, "api", "read_user", "profile", "email"}
|
||||
|
||||
const (
|
||||
defaultProviderURL = "https://gitlab.com"
|
||||
|
||||
// groupPath is the url to return a list of groups for the authenticated user
|
||||
// https://docs.gitlab.com/ee/api/groups.html
|
||||
groupPath = "/api/v4/groups"
|
||||
)
|
||||
|
||||
// Provider is a Gitlab implementation of the Authenticator interface.
|
||||
type Provider struct {
|
||||
*pom_oidc.Provider
|
||||
|
||||
userGroupURL string
|
||||
}
|
||||
|
||||
// New instantiates an OpenID Connect (OIDC) provider for Gitlab.
|
||||
func New(ctx context.Context, o *oauth.Options) (*Provider, error) {
|
||||
var p Provider
|
||||
var err error
|
||||
if o.ProviderURL == "" {
|
||||
o.ProviderURL = defaultProviderURL
|
||||
}
|
||||
if len(o.Scopes) == 0 {
|
||||
o.Scopes = defaultScopes
|
||||
}
|
||||
genericOidc, err := pom_oidc.New(ctx, o)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed creating oidc provider: %w", Name, err)
|
||||
}
|
||||
p.Provider = genericOidc
|
||||
p.UserGroupFn = p.UserGroups
|
||||
p.userGroupURL = o.ProviderURL + groupPath
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// UserGroups returns a slice of groups for the user.
|
||||
//
|
||||
// Returns 20 results at a time because the API results are paginated.
|
||||
// https://docs.gitlab.com/ee/api/groups.html#list-groups
|
||||
func (p *Provider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
||||
if s == nil || s.AccessToken == nil {
|
||||
return nil, errors.New("gitlab: user session cannot be empty")
|
||||
}
|
||||
|
||||
var response []struct {
|
||||
ID json.Number `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Visibility string `json:"visibility,omitempty"`
|
||||
ShareWithGroupLock bool `json:"share_with_group_lock,omitempty"`
|
||||
RequireTwoFactorAuthentication bool `json:"require_two_factor_authentication,omitempty"`
|
||||
SubgroupCreationLevel string `json:"subgroup_creation_level,omitempty"`
|
||||
FullName string `json:"full_name,omitempty"`
|
||||
FullPath string `json:"full_path,omitempty"`
|
||||
}
|
||||
headers := map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.AccessToken.AccessToken)}
|
||||
err := httputil.Client(ctx, http.MethodGet, p.userGroupURL, version.UserAgent(), headers, nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var groups []string
|
||||
log.Debug().Interface("response", response).Msg("gitlab: groups")
|
||||
for _, group := range response {
|
||||
groups = append(groups, group.ID.String())
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
package identity
|
||||
// Package google implements OpenID Connect for Google and GSuite.
|
||||
//
|
||||
// https://www.pomerium.io/docs/identity-providers/google.html
|
||||
// https://developers.google.com/identity/protocols/oauth2/openid-connect
|
||||
package google
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -11,63 +15,57 @@ import (
|
|||
"golang.org/x/oauth2/google"
|
||||
admin "google.golang.org/api/admin/directory/v1"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/identity/oauth"
|
||||
pom_oidc "github.com/pomerium/pomerium/internal/identity/oidc"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
)
|
||||
|
||||
const defaultGoogleProviderURL = "https://accounts.google.com"
|
||||
const (
|
||||
// Name identifies the Google identity provider
|
||||
Name = "google"
|
||||
|
||||
// GoogleProvider is an implementation of the Provider interface.
|
||||
type GoogleProvider struct {
|
||||
*Provider
|
||||
defaultProviderURL = "https://accounts.google.com"
|
||||
)
|
||||
|
||||
var defaultScopes = []string{oidc.ScopeOpenID, "profile", "email"}
|
||||
|
||||
// Provider is a Google implementation of the Authenticator interface.
|
||||
type Provider struct {
|
||||
*pom_oidc.Provider
|
||||
|
||||
// todo(bdd): we could probably save on a big ol set of imports
|
||||
// by calling this API directly
|
||||
apiClient *admin.Service
|
||||
}
|
||||
|
||||
// NewGoogleProvider instantiates an OpenID Connect (OIDC) session with Google.
|
||||
func NewGoogleProvider(p *Provider) (*GoogleProvider, error) {
|
||||
ctx := context.Background()
|
||||
if p.ProviderURL == "" {
|
||||
p.ProviderURL = defaultGoogleProviderURL
|
||||
}
|
||||
// New instantiates an OpenID Connect (OIDC) session with Google.
|
||||
func New(ctx context.Context, o *oauth.Options) (*Provider, error) {
|
||||
var p Provider
|
||||
var err error
|
||||
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
|
||||
if o.ProviderURL == "" {
|
||||
o.ProviderURL = defaultProviderURL
|
||||
}
|
||||
if len(o.Scopes) == 0 {
|
||||
o.Scopes = defaultScopes
|
||||
}
|
||||
genericOidc, err := pom_oidc.New(ctx, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Google rejects the offline scope favoring "access_type=offline"
|
||||
// as part of the authorization request instead.
|
||||
if len(p.Scopes) == 0 {
|
||||
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email"}
|
||||
}
|
||||
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,
|
||||
return nil, fmt.Errorf("%s: failed creating oidc provider: %w", Name, err)
|
||||
}
|
||||
p.Provider = genericOidc
|
||||
|
||||
gp := &GoogleProvider{
|
||||
Provider: p,
|
||||
}
|
||||
|
||||
// build api client to make group membership api calls
|
||||
if err := p.provider.Claims(&gp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if service account set, configure admin sdk calls
|
||||
if p.ServiceAccount != "" {
|
||||
apiCreds, err := base64.StdEncoding.DecodeString(p.ServiceAccount)
|
||||
if o.ServiceAccount != "" {
|
||||
apiCreds, err := base64.StdEncoding.DecodeString(o.ServiceAccount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identity/google: could not decode service account json %w", err)
|
||||
return nil, fmt.Errorf("google: could not decode service account json %w", err)
|
||||
}
|
||||
// Required scopes for groups api
|
||||
// https://developers.google.com/admin-sdk/directory/v1/reference/groups/list
|
||||
conf, err := google.JWTConfigFromJSON(apiCreds, admin.AdminDirectoryUserReadonlyScope, admin.AdminDirectoryGroupReadonlyScope)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identity/google: failed making jwt config from json %w", err)
|
||||
return nil, fmt.Errorf("google: failed making jwt config from json %w", err)
|
||||
}
|
||||
var credentialsFile struct {
|
||||
ImpersonateUser string `json:"impersonate_user"`
|
||||
|
@ -77,16 +75,16 @@ func NewGoogleProvider(p *Provider) (*GoogleProvider, error) {
|
|||
}
|
||||
conf.Subject = credentialsFile.ImpersonateUser
|
||||
client := conf.Client(context.TODO())
|
||||
gp.apiClient, err = admin.New(client)
|
||||
p.apiClient, err = admin.New(client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identity/google: failed creating admin service %w", err)
|
||||
return nil, fmt.Errorf("google: failed creating admin service %w", err)
|
||||
}
|
||||
gp.UserGroupFn = gp.UserGroups
|
||||
p.UserGroupFn = p.UserGroups
|
||||
} else {
|
||||
log.Warn().Msg("identity/google: no service account, cannot retrieve groups")
|
||||
log.Warn().Msg("google: no service account, cannot retrieve groups")
|
||||
}
|
||||
|
||||
return gp, nil
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// GetSignInURL returns a URL to OAuth 2.0 provider's consent page that asks for permissions for
|
||||
|
@ -100,21 +98,21 @@ func NewGoogleProvider(p *Provider) (*GoogleProvider, error) {
|
|||
// cookies, re-authorization will not bring back refresh_token. A work around to this is to add
|
||||
// prompt=consent to the OAuth redirect URL and will always return a refresh_token.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||
func (p *GoogleProvider) GetSignInURL(state string) string {
|
||||
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "select_account consent"))
|
||||
func (p *Provider) GetSignInURL(state string) string {
|
||||
return p.Oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "select_account consent"))
|
||||
}
|
||||
|
||||
// UserGroups returns a slice of group names a given user is in
|
||||
// NOTE: groups via Directory API is limited to 1 QPS!
|
||||
// https://developers.google.com/admin-sdk/directory/v1/reference/groups/list
|
||||
// https://developers.google.com/admin-sdk/directory/v1/limits
|
||||
func (p *GoogleProvider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
||||
func (p *Provider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
||||
var groups []string
|
||||
if p.apiClient != nil {
|
||||
req := p.apiClient.Groups.List().UserKey(s.Subject).MaxResults(100)
|
||||
resp, err := req.Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identity/google: group api request failed %w", err)
|
||||
return nil, fmt.Errorf("google: group api request failed %w", err)
|
||||
}
|
||||
for _, group := range resp.Groups {
|
||||
groups = append(groups, group.Email)
|
216
internal/identity/oidc/oidc.go
Normal file
216
internal/identity/oidc/oidc.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
// Package oidc implements a generic OpenID Connect provider.
|
||||
//
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
go_oidc "github.com/coreos/go-oidc"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/identity/oauth"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
// Name identifies the generic OpenID Connect provider
|
||||
const Name = "oidc"
|
||||
|
||||
var defaultScopes = []string{go_oidc.ScopeOpenID, "profile", "email", "offline_access"}
|
||||
|
||||
// Provider provides a standard, OpenID Connect implementation
|
||||
// of an authorization identity provider.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html
|
||||
type Provider struct {
|
||||
// Provider represents an OpenID Connect server's configuration.
|
||||
Provider *go_oidc.Provider
|
||||
// Verifier provides verification for ID Tokens.
|
||||
Verifier *go_oidc.IDTokenVerifier
|
||||
// Oauth describes a typical 3-legged OAuth2 flow, with both the
|
||||
// client application information and the server's endpoint URLs.
|
||||
Oauth *oauth2.Config
|
||||
|
||||
// UserInfoURL specifies the endpoint responsible for returning claims
|
||||
// about the authenticated End-User.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||
UserInfoURL string `json:"userinfo_endpoint,omitempty"`
|
||||
|
||||
// RevocationURL is the location of the OAuth 2.0 token revocation endpoint.
|
||||
// https://tools.ietf.org/html/rfc7009
|
||||
RevocationURL string `json:"revocation_endpoint,omitempty"`
|
||||
|
||||
// EndSessionURL is another endpoint that can be used by other identity
|
||||
// providers that doesn't implement the revocation endpoint but a logout session.
|
||||
// https://openid.net/specs/openid-connect-frontchannel-1_0.html#RPInitiated
|
||||
EndSessionURL string `json:"end_session_endpoint,omitempty"`
|
||||
|
||||
// UserGroupFn is, if set, used to return a slice of group IDs the
|
||||
// user is a member of
|
||||
UserGroupFn func(context.Context, *sessions.State) ([]string, error)
|
||||
}
|
||||
|
||||
// New creates a new instance of a generic OpenID Connect provider.
|
||||
func New(ctx context.Context, o *oauth.Options) (*Provider, error) {
|
||||
var err error
|
||||
var p Provider
|
||||
if o.ProviderURL == "" {
|
||||
return nil, ErrMissingProviderURL
|
||||
}
|
||||
if len(o.Scopes) == 0 {
|
||||
o.Scopes = defaultScopes
|
||||
}
|
||||
p.Provider, err = go_oidc.NewProvider(ctx, o.ProviderURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identity/oidc: could not connect to %s: %w", o.ProviderName, err)
|
||||
}
|
||||
|
||||
p.Verifier = p.Provider.Verifier(&go_oidc.Config{ClientID: o.ClientID})
|
||||
p.Oauth = &oauth2.Config{
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
Scopes: o.Scopes,
|
||||
Endpoint: p.Provider.Endpoint(),
|
||||
RedirectURL: o.RedirectURL.String(),
|
||||
}
|
||||
|
||||
// add non-standard claims like end-session, revoke, and user info
|
||||
if err := p.Provider.Claims(&p); err != nil {
|
||||
return nil, fmt.Errorf("identity/oidc: could not retrieve additional claims: %w", err)
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// GetSignInURL returns the url of the provider's OAuth 2.0 consent page
|
||||
// that asks for permissions for the required scopes explicitly.
|
||||
//
|
||||
// State is a token to protect the user from CSRF attacks. You must
|
||||
// always provide a non-empty string and validate that it matches the
|
||||
// the state query parameter on your redirect callback.
|
||||
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
||||
func (p *Provider) GetSignInURL(state string) string {
|
||||
return p.Oauth.AuthCodeURL(state, oauth2.AccessTypeOffline)
|
||||
}
|
||||
|
||||
// Authenticate converts an authorization code returned from the identity
|
||||
// provider into a token which is then converted into a user session.
|
||||
func (p *Provider) Authenticate(ctx context.Context, code string) (*sessions.State, error) {
|
||||
oauth2Token, err := p.Oauth.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identity/oidc: token exchange failed: %w", err)
|
||||
}
|
||||
idToken, err := p.IdentityFromToken(ctx, oauth2Token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identity/oidc: failed getting id_token: %w", err)
|
||||
}
|
||||
|
||||
aud, err := urlutil.ParseAndValidateURL(p.Oauth.RedirectURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identity/oidc: bad redirect uri: %w", err)
|
||||
}
|
||||
|
||||
s, err := sessions.NewStateFromTokens(idToken, oauth2Token, aud.Hostname())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := p.Provider.Claims(&p); err == nil && p.UserInfoURL != "" {
|
||||
userInfo, err := p.Provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identity/oidc: could not retrieve user info %w", err)
|
||||
}
|
||||
if err := userInfo.Claims(&s); err != nil {
|
||||
return nil, fmt.Errorf("identity/oidc: could not parse user claims %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if p.UserGroupFn != nil {
|
||||
s.Groups, err = p.UserGroupFn(ctx, s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal/oidc: could not retrieve groups %w", err)
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Refresh renews a user's session using an oidc refresh token without reprompting the user.
|
||||
// Group membership is also refreshed.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
|
||||
func (p *Provider) Refresh(ctx context.Context, s *sessions.State) (*sessions.State, error) {
|
||||
if s.AccessToken == nil || s.AccessToken.RefreshToken == "" {
|
||||
return nil, errors.New("internal/oidc: missing refresh token")
|
||||
}
|
||||
|
||||
t := oauth2.Token{RefreshToken: s.AccessToken.RefreshToken}
|
||||
oauthToken, err := p.Oauth.TokenSource(ctx, &t).Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal/oidc: refresh failed %w", err)
|
||||
}
|
||||
idToken, err := p.IdentityFromToken(ctx, oauthToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identity/oidc: failed getting id_token: %w", err)
|
||||
}
|
||||
if err := s.UpdateState(idToken, oauthToken); err != nil {
|
||||
return nil, fmt.Errorf("internal/oidc: state update failed %w", err)
|
||||
}
|
||||
if p.UserGroupFn != nil {
|
||||
s.Groups, err = p.UserGroupFn(ctx, s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal/oidc: could not retrieve groups %w", err)
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// IdentityFromToken takes an identity provider issued JWT as input ('id_token')
|
||||
// and returns a session state. The provided token's audience ('aud') must
|
||||
// match Pomerium's client_id.
|
||||
func (p *Provider) IdentityFromToken(ctx context.Context, t *oauth2.Token) (*go_oidc.IDToken, error) {
|
||||
rawIDToken, ok := t.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("internal/oidc: id_token not found")
|
||||
}
|
||||
return p.Verifier.Verify(ctx, rawIDToken)
|
||||
}
|
||||
|
||||
// Revoke enables a user to revoke her token. If the identity provider does not
|
||||
// support revocation an error is thrown.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7009#section-2.1
|
||||
func (p *Provider) Revoke(ctx context.Context, token *oauth2.Token) error {
|
||||
if p.RevocationURL == "" {
|
||||
return ErrRevokeNotImplemented
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Add("token", token.AccessToken)
|
||||
params.Add("token_type_hint", "access_token")
|
||||
// Some providers like okta / onelogin require "client authentication"
|
||||
// https://developer.okta.com/docs/reference/api/oidc/#client-secret
|
||||
// https://developers.onelogin.com/openid-connect/api/revoke-session
|
||||
params.Add("client_id", p.Oauth.ClientID)
|
||||
params.Add("client_secret", p.Oauth.ClientSecret)
|
||||
|
||||
err := httputil.Client(ctx, http.MethodPost, p.RevocationURL, version.UserAgent(), nil, params, nil)
|
||||
if err != nil && err != httputil.ErrTokenRevoked {
|
||||
return fmt.Errorf("internal/oidc: unexpected revoke error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogOut returns the EndSessionURL endpoint to allow a logout
|
||||
// session to be initiated.
|
||||
// https://openid.net/specs/openid-connect-frontchannel-1_0.html#RPInitiated
|
||||
func (p *Provider) LogOut() (*url.URL, error) {
|
||||
if p.EndSessionURL == "" {
|
||||
return nil, ErrSignoutNotImplemented
|
||||
}
|
||||
return urlutil.ParseAndValidateURL(p.EndSessionURL)
|
||||
}
|
88
internal/identity/oidc/okta/okta.go
Normal file
88
internal/identity/oidc/okta/okta.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
// Package okta implements OpenID Connect for okta
|
||||
//
|
||||
// https://www.pomerium.io/docs/identity-providers/okta.html
|
||||
package okta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/identity/oauth"
|
||||
pom_oidc "github.com/pomerium/pomerium/internal/identity/oidc"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
// Name identifies the Okta identity provider
|
||||
Name = "okta"
|
||||
|
||||
// https://developer.okta.com/docs/reference/api/users/
|
||||
userAPIPath = "/api/v1/users/"
|
||||
)
|
||||
|
||||
// Provider is an Okta implementation of the Authenticator interface.
|
||||
type Provider struct {
|
||||
*pom_oidc.Provider
|
||||
|
||||
userAPI *url.URL
|
||||
|
||||
// serviceAccount is the the custom HTTP authentication used for okta
|
||||
// https://developer.okta.com/docs/reference/api-overview/#authentication
|
||||
serviceAccount string
|
||||
}
|
||||
|
||||
// New instantiates an OpenID Connect (OIDC) provider for Okta.
|
||||
func New(ctx context.Context, o *oauth.Options) (*Provider, error) {
|
||||
var p Provider
|
||||
var err error
|
||||
genericOidc, err := pom_oidc.New(ctx, o)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed creating oidc provider: %w", Name, err)
|
||||
}
|
||||
p.Provider = genericOidc
|
||||
|
||||
if o.ServiceAccount != "" {
|
||||
userAPI, err := urlutil.ParseAndValidateURL(o.ProviderURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.userAPI = userAPI
|
||||
p.userAPI.Path = userAPIPath
|
||||
p.serviceAccount = o.ServiceAccount
|
||||
p.UserGroupFn = p.UserGroups
|
||||
} else {
|
||||
log.Warn().Msg("okta: api token not set, cannot retrieve groups")
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// UserGroups fetches the groups of which the user is a member
|
||||
// https://developer.okta.com/docs/reference/api/users/#get-user-s-groups
|
||||
func (p *Provider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
||||
var response []struct {
|
||||
ID string `json:"id"`
|
||||
Profile struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
} `json:"profile"`
|
||||
}
|
||||
|
||||
headers := map[string]string{"Authorization": fmt.Sprintf("SSWS %s", p.serviceAccount)}
|
||||
uri := fmt.Sprintf("%s/%s/groups", p.userAPI.String(), s.Subject)
|
||||
err := httputil.Client(ctx, http.MethodGet, uri, version.UserAgent(), headers, nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var groups []string
|
||||
for _, group := range response {
|
||||
log.Debug().Interface("group", group).Msg("okta: group")
|
||||
groups = append(groups, group.ID)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
78
internal/identity/oidc/onelogin/onelogin.go
Normal file
78
internal/identity/oidc/onelogin/onelogin.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
// Package onelogin implements OpenID Connect for OneLogin
|
||||
//
|
||||
// https://www.pomerium.io/docs/identity-providers/one-login.html
|
||||
package onelogin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/identity/oauth"
|
||||
pom_oidc "github.com/pomerium/pomerium/internal/identity/oidc"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
// Name identifies the OneLogin identity provider
|
||||
Name = "onelogin"
|
||||
|
||||
defaultProviderURL = "https://openid-connect.onelogin.com/oidc"
|
||||
defaultOneloginGroupURL = "https://openid-connect.onelogin.com/oidc/me"
|
||||
)
|
||||
|
||||
var defaultScopes = []string{oidc.ScopeOpenID, "profile", "email", "groups", "offline_access"}
|
||||
|
||||
// Provider is an OneLogin implementation of the Authenticator interface.
|
||||
type Provider struct {
|
||||
*pom_oidc.Provider
|
||||
}
|
||||
|
||||
// New instantiates an OpenID Connect (OIDC) provider for OneLogin.
|
||||
func New(ctx context.Context, o *oauth.Options) (*Provider, error) {
|
||||
var p Provider
|
||||
var err error
|
||||
if o.ProviderURL == "" {
|
||||
o.ProviderURL = defaultProviderURL
|
||||
}
|
||||
if len(o.Scopes) == 0 {
|
||||
o.Scopes = defaultScopes
|
||||
}
|
||||
genericOidc, err := pom_oidc.New(ctx, o)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed creating oidc provider: %w", Name, err)
|
||||
}
|
||||
p.Provider = genericOidc
|
||||
p.UserGroupFn = p.UserGroups
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// UserGroups returns a slice of group names a given user is in.
|
||||
// https://developers.onelogin.com/openid-connect/api/user-info
|
||||
func (p *Provider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
||||
if s == nil || s.AccessToken == nil {
|
||||
return nil, errors.New("identity/onelogin: session cannot be nil")
|
||||
}
|
||||
var response struct {
|
||||
User string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Name string `json:"name"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
GivenName string `json:"given_name"`
|
||||
FamilyName string `json:"family_name"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
headers := map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.AccessToken.AccessToken)}
|
||||
err := httputil.Client(ctx, http.MethodGet, defaultOneloginGroupURL, version.UserAgent(), headers, nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response.Groups, nil
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
// OktaProvider represents the Okta Identity Provider
|
||||
//
|
||||
// https://www.pomerium.io/docs/identity-providers.html#okta
|
||||
type OktaProvider struct {
|
||||
*Provider
|
||||
|
||||
userAPI *url.URL
|
||||
}
|
||||
|
||||
// NewOktaProvider creates a new instance of Okta as an identity provider.
|
||||
func NewOktaProvider(p *Provider) (*OktaProvider, error) {
|
||||
ctx := context.Background()
|
||||
if p.ProviderURL == "" {
|
||||
return nil, ErrMissingProviderURL
|
||||
}
|
||||
var err error
|
||||
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(p.Scopes) == 0 {
|
||||
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "groups", "offline_access"}
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
oktaProvider := OktaProvider{Provider: p}
|
||||
if err := p.provider.Claims(&oktaProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.ServiceAccount != "" {
|
||||
p.UserGroupFn = oktaProvider.UserGroups
|
||||
userAPI, err := urlutil.ParseAndValidateURL(p.ProviderURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userAPI.Path = "/api/v1/users/"
|
||||
oktaProvider.userAPI = userAPI
|
||||
} else {
|
||||
log.Warn().Msg("identity/okta: api token not set, cannot retrieve groups")
|
||||
}
|
||||
|
||||
return &oktaProvider, nil
|
||||
}
|
||||
|
||||
// UserGroups fetches the groups of which the user is a member
|
||||
// https://developer.okta.com/docs/reference/api/users/#get-user-s-groups
|
||||
func (p *OktaProvider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
||||
var response []struct {
|
||||
ID string `json:"id"`
|
||||
Profile struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
} `json:"profile"`
|
||||
}
|
||||
|
||||
headers := map[string]string{"Authorization": fmt.Sprintf("SSWS %s", p.ServiceAccount)}
|
||||
err := httputil.Client(ctx, http.MethodGet, fmt.Sprintf("%s/%s/groups", p.userAPI.String(), s.Subject), version.UserAgent(), headers, nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var groups []string
|
||||
for _, group := range response {
|
||||
log.Debug().Interface("group", group).Msg("identity/okta: group")
|
||||
groups = append(groups, group.ID)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
const defaultOneLoginProviderURL = "https://openid-connect.onelogin.com/oidc"
|
||||
const defaultOneloginGroupURL = "https://openid-connect.onelogin.com/oidc/me"
|
||||
|
||||
// OneLoginProvider provides a standard, OpenID Connect implementation
|
||||
// of an authorization identity provider.
|
||||
type OneLoginProvider struct {
|
||||
*Provider
|
||||
}
|
||||
|
||||
// NewOneLoginProvider creates a new instance of an OpenID Connect provider.
|
||||
func NewOneLoginProvider(p *Provider) (*OneLoginProvider, error) {
|
||||
ctx := context.Background()
|
||||
if p.ProviderURL == "" {
|
||||
p.ProviderURL = defaultOneLoginProviderURL
|
||||
}
|
||||
var err error
|
||||
p.provider, err = oidc.NewProvider(ctx, p.ProviderURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(p.Scopes) == 0 {
|
||||
p.Scopes = []string{oidc.ScopeOpenID, "profile", "email", "groups", "offline_access"}
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
olProvider := OneLoginProvider{Provider: p}
|
||||
|
||||
if err := p.provider.Claims(&olProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.UserGroupFn = olProvider.UserGroups
|
||||
|
||||
return &olProvider, nil
|
||||
}
|
||||
|
||||
// UserGroups returns a slice of group names a given user is in.
|
||||
// https://developers.onelogin.com/openid-connect/api/user-info
|
||||
func (p *OneLoginProvider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
||||
if s == nil || s.AccessToken == nil {
|
||||
return nil, errors.New("identity/onelogin: session cannot be nil")
|
||||
}
|
||||
var response struct {
|
||||
User string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Name string `json:"name"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
GivenName string `json:"given_name"`
|
||||
FamilyName string `json:"family_name"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
headers := map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.AccessToken.AccessToken)}
|
||||
err := httputil.Client(ctx, http.MethodGet, defaultOneloginGroupURL, version.UserAgent(), headers, nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response.Groups, nil
|
||||
}
|
|
@ -4,41 +4,34 @@ package identity
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/urlutil"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/identity/oauth"
|
||||
"github.com/pomerium/pomerium/internal/identity/oauth/github"
|
||||
"github.com/pomerium/pomerium/internal/identity/oidc"
|
||||
"github.com/pomerium/pomerium/internal/identity/oidc/azure"
|
||||
"github.com/pomerium/pomerium/internal/identity/oidc/gitlab"
|
||||
"github.com/pomerium/pomerium/internal/identity/oidc/google"
|
||||
"github.com/pomerium/pomerium/internal/identity/oidc/okta"
|
||||
"github.com/pomerium/pomerium/internal/identity/oidc/onelogin"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
)
|
||||
|
||||
const (
|
||||
// AzureProviderName identifies the Azure identity provider
|
||||
AzureProviderName = "azure"
|
||||
// GitlabProviderName identifies the GitLab identity provider
|
||||
GitlabProviderName = "gitlab"
|
||||
// GithubProviderName identifies the GitHub identity provider
|
||||
GithubProviderName = "github"
|
||||
// GoogleProviderName identifies the Google identity provider
|
||||
GoogleProviderName = "google"
|
||||
// OIDCProviderName identifies a generic OpenID connect provider
|
||||
OIDCProviderName = "oidc"
|
||||
// OktaProviderName identifies the Okta identity provider
|
||||
OktaProviderName = "okta"
|
||||
// OneLoginProviderName identifies the OneLogin identity provider
|
||||
OneLoginProviderName = "onelogin"
|
||||
var (
|
||||
// compile time assertions that providers are satisfying the interface
|
||||
_ Authenticator = &azure.Provider{}
|
||||
_ Authenticator = &gitlab.Provider{}
|
||||
_ Authenticator = &github.Provider{}
|
||||
_ Authenticator = &google.Provider{}
|
||||
_ Authenticator = &oidc.Provider{}
|
||||
_ Authenticator = &okta.Provider{}
|
||||
_ Authenticator = &onelogin.Provider{}
|
||||
_ Authenticator = &MockProvider{}
|
||||
)
|
||||
|
||||
// ErrMissingProviderURL is returned when an identity provider requires a provider url
|
||||
// does not receive one.
|
||||
var ErrMissingProviderURL = errors.New("internal/identity: missing provider url")
|
||||
|
||||
// Authenticator is an interface representing the ability to authenticate with an identity provider.
|
||||
type Authenticator interface {
|
||||
Authenticate(context.Context, string) (*sessions.State, error)
|
||||
|
@ -48,195 +41,29 @@ type Authenticator interface {
|
|||
LogOut() (*url.URL, error)
|
||||
}
|
||||
|
||||
// New returns a new identity provider based on its name.
|
||||
// Returns an error if selected provided not found or if the identity provider is not known.
|
||||
func New(providerName string, p *Provider) (a Authenticator, err error) {
|
||||
switch providerName {
|
||||
case AzureProviderName:
|
||||
a, err = NewAzureProvider(p)
|
||||
case GitlabProviderName:
|
||||
a, err = NewGitLabProvider(p)
|
||||
case GithubProviderName:
|
||||
a, err = NewGitHubProvider(p)
|
||||
case GoogleProviderName:
|
||||
a, err = NewGoogleProvider(p)
|
||||
case OIDCProviderName:
|
||||
a, err = NewOIDCProvider(p)
|
||||
case OktaProviderName:
|
||||
a, err = NewOktaProvider(p)
|
||||
case OneLoginProviderName:
|
||||
a, err = NewOneLoginProvider(p)
|
||||
// NewAuthenticator returns a new identity provider based on its name.
|
||||
func NewAuthenticator(o oauth.Options) (a Authenticator, err error) {
|
||||
ctx := context.Background()
|
||||
switch o.ProviderName {
|
||||
case azure.Name:
|
||||
a, err = azure.New(ctx, &o)
|
||||
case gitlab.Name:
|
||||
a, err = gitlab.New(ctx, &o)
|
||||
case github.Name:
|
||||
a, err = github.New(ctx, &o)
|
||||
case google.Name:
|
||||
a, err = google.New(ctx, &o)
|
||||
case oidc.Name:
|
||||
a, err = oidc.New(ctx, &o)
|
||||
case okta.Name:
|
||||
a, err = okta.New(ctx, &o)
|
||||
case onelogin.Name:
|
||||
a, err = onelogin.New(ctx, &o)
|
||||
default:
|
||||
return nil, fmt.Errorf("internal/identity: %s provider not known", providerName)
|
||||
return nil, fmt.Errorf("identity: unknown provider: %s", o.ProviderName)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Provider contains the fields required for an OAuth 2.0 Authorization Request that
|
||||
// requests that the End-User be authenticated by the Authorization Server.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
type Provider struct {
|
||||
ProviderName string
|
||||
|
||||
RedirectURL *url.URL
|
||||
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ProviderURL string
|
||||
Scopes []string
|
||||
|
||||
UserGroupFn func(context.Context, *sessions.State) ([]string, error)
|
||||
|
||||
UserInfoEndpoint bool
|
||||
|
||||
// ServiceAccount can be set for those providers that require additional
|
||||
// credentials or tokens to do follow up API calls (e.g. Google)
|
||||
ServiceAccount string
|
||||
|
||||
provider *oidc.Provider
|
||||
verifier *oidc.IDTokenVerifier
|
||||
oauth *oauth2.Config
|
||||
|
||||
// We will attempt to get the identity provider's possible information from
|
||||
// their /.well-known/openid-configuration.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||
UserInfoURL string `json:"userinfo_endpoint"`
|
||||
|
||||
// RevocationURL is the location of the OAuth 2.0 token revocation endpoint.
|
||||
// https://tools.ietf.org/html/rfc7009
|
||||
RevocationURL string `json:"revocation_endpoint,omitempty"`
|
||||
|
||||
// EndSessionURL is another endpoint that can be used by other identity
|
||||
// providers that doesn't implement the revocation endpoint but a logout session.
|
||||
// https://openid.net/specs/openid-connect-frontchannel-1_0.html#RPInitiated
|
||||
// e.g Microsoft Azure
|
||||
// https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
|
||||
EndSessionURL string `json:"end_session_endpoint,omitempty"`
|
||||
}
|
||||
|
||||
// GetSignInURL returns a URL to OAuth 2.0 provider's consent page
|
||||
// that asks for permissions for the required scopes explicitly.
|
||||
//
|
||||
// State is a token to protect the user from CSRF attacks. You must
|
||||
// always provide a non-empty string and validate that it matches the
|
||||
// the state query parameter on your redirect callback.
|
||||
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
||||
func (p *Provider) GetSignInURL(state string) string {
|
||||
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline)
|
||||
}
|
||||
|
||||
// Authenticate creates an identity session with google from a authorization code, and follows up
|
||||
// call to the admin/group api to check what groups the user is in.
|
||||
func (p *Provider) Authenticate(ctx context.Context, code string) (*sessions.State, error) {
|
||||
oauth2Token, err := p.oauth.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal/identity: token exchange failed: %w", err)
|
||||
}
|
||||
idToken, err := p.IdentityFromToken(ctx, oauth2Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := sessions.NewStateFromTokens(idToken, oauth2Token, p.RedirectURL.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := p.provider.Claims(&p); err == nil && p.UserInfoURL != "" {
|
||||
userInfo, err := p.provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal/identity: could not retrieve user info %w", err)
|
||||
}
|
||||
if err := userInfo.Claims(&s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if p.UserGroupFn != nil {
|
||||
s.Groups, err = p.UserGroupFn(ctx, s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal/identity: could not retrieve groups %w", err)
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Refresh renews a user's session using an oidc refresh token without reprompting the user.
|
||||
// Group membership is also refreshed.
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
|
||||
func (p *Provider) Refresh(ctx context.Context, s *sessions.State) (*sessions.State, error) {
|
||||
if s.AccessToken == nil || s.AccessToken.RefreshToken == "" {
|
||||
return nil, errors.New("internal/identity: missing refresh token")
|
||||
}
|
||||
|
||||
t := oauth2.Token{RefreshToken: s.AccessToken.RefreshToken}
|
||||
oauthToken, err := p.oauth.TokenSource(ctx, &t).Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal/identity: refresh failed %w", err)
|
||||
}
|
||||
idToken, err := p.IdentityFromToken(ctx, oauthToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.UpdateState(idToken, oauthToken); err != nil {
|
||||
return nil, fmt.Errorf("internal/identity: state update failed %w", err)
|
||||
}
|
||||
if p.UserGroupFn != nil {
|
||||
s.Groups, err = p.UserGroupFn(ctx, s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("internal/identity: could not retrieve groups %w", err)
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// IdentityFromToken takes an identity provider issued JWT as input ('id_token')
|
||||
// and returns a session state. The provided token's audience ('aud') must
|
||||
// match Pomerium's client_id.
|
||||
func (p *Provider) IdentityFromToken(ctx context.Context, t *oauth2.Token) (*oidc.IDToken, error) {
|
||||
rawIDToken, ok := t.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("internal/identity: id_token not found")
|
||||
}
|
||||
return p.verifier.Verify(ctx, rawIDToken)
|
||||
}
|
||||
|
||||
// Revoke enables a user to revoke her token. If the identity provider does not
|
||||
// support revocation an error is thrown.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7009
|
||||
func (p *Provider) Revoke(ctx context.Context, token *oauth2.Token) error {
|
||||
if p.RevocationURL == "" {
|
||||
return ErrRevokeNotImplemented
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
// https://tools.ietf.org/html/rfc7009#section-2.1
|
||||
params.Add("token", token.AccessToken)
|
||||
params.Add("token_type_hint", "access_token")
|
||||
// Some providers like okta / onelogin require "client authentication"
|
||||
// https://developer.okta.com/docs/reference/api/oidc/#client-secret
|
||||
// https://developers.onelogin.com/openid-connect/api/revoke-session
|
||||
params.Add("client_id", p.ClientID)
|
||||
params.Add("client_secret", p.ClientSecret)
|
||||
|
||||
err := httputil.Client(ctx, http.MethodPost, p.RevocationURL, version.UserAgent(), nil, params, nil)
|
||||
if err != nil && err != httputil.ErrTokenRevoked {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogOut returns the EndSessionURL endpoint to allow a logout
|
||||
// session to be initiated.
|
||||
// https://openid.net/specs/openid-connect-frontchannel-1_0.html#RPInitiated
|
||||
func (p *Provider) LogOut() (*url.URL, error) {
|
||||
if p.EndSessionURL == "" {
|
||||
return nil, ErrSignoutNotImplemented
|
||||
}
|
||||
return urlutil.ParseAndValidateURL(p.EndSessionURL)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue