mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-07 13:22:43 +02:00
identity/provider: implement generic revoke method (#595)
Co-authored-by: Bobby DeSimone <bobbydesimone@gmail.com>
This commit is contained in:
parent
45c706666c
commit
75f4dadad6
12 changed files with 141 additions and 133 deletions
|
@ -217,6 +217,9 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) error {
|
|||
// SignOut signs the user out and attempts to revoke the user's identity session
|
||||
// Handles both GET and POST.
|
||||
func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) error {
|
||||
// no matter what happens, we want to clear the local session store
|
||||
defer a.sessionStore.ClearSession(w, r)
|
||||
|
||||
jwt, err := sessions.FromContext(r.Context())
|
||||
if err != nil {
|
||||
return httputil.NewError(http.StatusBadRequest, err)
|
||||
|
@ -226,17 +229,28 @@ func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) error {
|
|||
return httputil.NewError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
a.sessionStore.ClearSession(w, r)
|
||||
redirectString := r.FormValue(urlutil.QueryRedirectURI)
|
||||
|
||||
// first, try to revoke the session if implemented
|
||||
err = a.provider.Revoke(r.Context(), s.AccessToken)
|
||||
if errors.Is(err, identity.ErrRevokeNotImplemented) {
|
||||
log.FromRequest(r).Warn().Err(err).Msg("authenticate: revoke not implemented")
|
||||
} else if err != nil {
|
||||
if err != nil && !errors.Is(err, identity.ErrRevokeNotImplemented) {
|
||||
return httputil.NewError(http.StatusBadRequest, err)
|
||||
}
|
||||
redirectURL, err := urlutil.ParseAndValidateURL(r.FormValue(urlutil.QueryRedirectURI))
|
||||
|
||||
// next, try to build a logout url if implemented
|
||||
endSessionURL, err := a.provider.LogOut()
|
||||
if err == nil {
|
||||
params := url.Values{}
|
||||
params.Add("post_logout_redirect_uri", redirectString)
|
||||
endSessionURL.RawQuery = params.Encode()
|
||||
redirectString = endSessionURL.String()
|
||||
} else if !errors.Is(err, identity.ErrSignoutNotImplemented) {
|
||||
return httputil.NewError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
redirectURL, err := urlutil.ParseAndValidateURL(redirectString)
|
||||
if err != nil {
|
||||
return httputil.NewError(http.StatusBadRequest, err)
|
||||
|
||||
}
|
||||
httputil.Redirect(w, r, redirectURL.String(), http.StatusFound)
|
||||
return nil
|
||||
|
|
|
@ -189,10 +189,10 @@ func TestAuthenticate_SignOut(t *testing.T) {
|
|||
wantCode int
|
||||
wantBody string
|
||||
}{
|
||||
{"good post", http.MethodPost, nil, "https://corp.pomerium.io/", "sig", "ts", identity.MockProvider{}, &mstore.Store{Encrypted: true, Session: &sessions.State{Email: "user@pomerium.io", AccessToken: &oauth2.Token{Expiry: time.Now().Add(10 * time.Second)}}}, http.StatusFound, ""},
|
||||
{"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{}, &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: 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"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
@ -64,7 +64,7 @@ type Policy struct {
|
|||
TLSCustomCAFile string `mapstructure:"tls_custom_ca_file" yaml:"tls_custom_ca_file,omitempty"`
|
||||
RootCAs *x509.CertPool `yaml:",omitempty"`
|
||||
|
||||
// Contains the x.509 client certificate to to present to the downstream
|
||||
// Contains the x.509 client certificate to present to the downstream
|
||||
// host.
|
||||
TLSClientCert string `mapstructure:"tls_client_cert" yaml:"tls_client_cert,omitempty"`
|
||||
TLSClientKey string `mapstructure:"tls_client_key" yaml:"tls_client_key,omitempty"`
|
||||
|
|
|
@ -5,3 +5,8 @@ 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")
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/httputil"
|
||||
|
@ -19,11 +21,11 @@ import (
|
|||
|
||||
const (
|
||||
defaultGitHubProviderURL = "https://github.com"
|
||||
githubAuthURL = "/login/oauth/authorize"
|
||||
githubUserURL = "https://api.github.com/user"
|
||||
githubUserTeamURL = "https://api.github.com/user/teams"
|
||||
githubRevokeURL = "https://github.com/oauth/revoke"
|
||||
githubUserEmailURL = "https://api.github.com/user/emails"
|
||||
githubAPIURL = "https://api.github.com"
|
||||
userPath = "/user"
|
||||
teamPath = "/user/teams"
|
||||
revokePath = "/applications/%s/grant"
|
||||
emailPath = "/user/emails"
|
||||
|
||||
// since github doesn't implement oidc, we need this to refresh the user session
|
||||
refreshDeadline = time.Minute * 60
|
||||
|
@ -33,22 +35,11 @@ const (
|
|||
type GitHubProvider struct {
|
||||
*Provider
|
||||
|
||||
authURL string
|
||||
tokenURL string
|
||||
userEndpoint string
|
||||
|
||||
RevokeURL string `json:"revocation_endpoint"`
|
||||
}
|
||||
|
||||
// NewGitHubProvider returns a new GitHubProvider.
|
||||
func NewGitHubProvider(p *Provider) (*GitHubProvider, error) {
|
||||
gp := &GitHubProvider{
|
||||
authURL: defaultGitHubProviderURL + githubAuthURL,
|
||||
tokenURL: defaultGitHubProviderURL + "/login/oauth/access_token",
|
||||
userEndpoint: githubUserURL,
|
||||
RevokeURL: githubRevokeURL,
|
||||
}
|
||||
|
||||
if p.ProviderURL == "" {
|
||||
p.ProviderURL = defaultGitHubProviderURL
|
||||
}
|
||||
|
@ -61,13 +52,16 @@ func NewGitHubProvider(p *Provider) (*GitHubProvider, error) {
|
|||
ClientID: p.ClientID,
|
||||
ClientSecret: p.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: gp.authURL,
|
||||
TokenURL: gp.tokenURL,
|
||||
AuthURL: p.ProviderURL + "/login/oauth/authorize",
|
||||
TokenURL: p.ProviderURL + "/login/oauth/access_token",
|
||||
},
|
||||
RedirectURL: p.RedirectURL.String(),
|
||||
Scopes: p.Scopes,
|
||||
}
|
||||
gp.Provider = p
|
||||
gp := &GitHubProvider{
|
||||
Provider: p,
|
||||
userEndpoint: githubAPIURL + userPath,
|
||||
}
|
||||
|
||||
return gp, nil
|
||||
}
|
||||
|
@ -107,7 +101,7 @@ func (p *GitHubProvider) updateSessionState(ctx context.Context, s *sessions.Sta
|
|||
|
||||
err := p.userInfo(ctx, accessToken, s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("identity/github: could not user info %w", err)
|
||||
return fmt.Errorf("identity/github: could not retrieve user info %w", err)
|
||||
}
|
||||
|
||||
err = p.userEmail(ctx, accessToken, s)
|
||||
|
@ -152,7 +146,8 @@ func (p *GitHubProvider) userTeams(ctx context.Context, at string, s *sessions.S
|
|||
}
|
||||
|
||||
headers := map[string]string{"Authorization": fmt.Sprintf("token %s", at)}
|
||||
err := httputil.Client(ctx, http.MethodGet, githubUserTeamURL, version.UserAgent(), headers, nil, &response)
|
||||
teamURL := githubAPIURL + teamPath
|
||||
err := httputil.Client(ctx, http.MethodGet, teamURL, version.UserAgent(), headers, nil, &response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -182,7 +177,8 @@ func (p *GitHubProvider) userEmail(ctx context.Context, at string, s *sessions.S
|
|||
Visibility string `json:"visibility"`
|
||||
}
|
||||
headers := map[string]string{"Authorization": fmt.Sprintf("token %s", at)}
|
||||
err := httputil.Client(ctx, http.MethodGet, githubUserEmailURL, version.UserAgent(), headers, nil, &response)
|
||||
emailURL := githubAPIURL + emailPath
|
||||
err := httputil.Client(ctx, http.MethodGet, emailURL, version.UserAgent(), headers, nil, &response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -223,3 +219,33 @@ func (p *GitHubProvider) userInfo(ctx context.Context, at string, s *sessions.St
|
|||
s.Expiry = jwt.NewNumericDate(time.Now().Add(refreshDeadline))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Revoke method will remove all the github grants the user
|
||||
// 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 {
|
||||
// build the basic authentication request
|
||||
basicAuth := url.UserPassword(p.ClientID, p.ClientSecret)
|
||||
revokeURL := url.URL{
|
||||
Scheme: "https",
|
||||
User: basicAuth,
|
||||
Host: "api.github.com",
|
||||
Path: fmt.Sprintf(revokePath, p.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")
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"golang.org/x/oauth2"
|
||||
|
@ -25,7 +24,6 @@ const (
|
|||
// GitLabProvider is an implementation of the OAuth Provider
|
||||
type GitLabProvider struct {
|
||||
*Provider
|
||||
RevokeURL string `json:"revocation_endpoint"`
|
||||
}
|
||||
|
||||
// NewGitLabProvider returns a new GitLabProvider.
|
||||
|
@ -55,9 +53,7 @@ func NewGitLabProvider(p *Provider) (*GitLabProvider, error) {
|
|||
RedirectURL: p.RedirectURL.String(),
|
||||
Scopes: p.Scopes,
|
||||
}
|
||||
gp := &GitLabProvider{
|
||||
Provider: p,
|
||||
}
|
||||
gp := &GitLabProvider{Provider: p}
|
||||
|
||||
if err := p.provider.Claims(&gp); err != nil {
|
||||
return nil, err
|
||||
|
@ -103,17 +99,3 @@ func (p *GitLabProvider) UserGroups(ctx context.Context, s *sessions.State) ([]s
|
|||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// Revoke attempts to revoke session access via revocation endpoint
|
||||
// https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#revoking-a-personal-access-token
|
||||
func (p *GitLabProvider) Revoke(ctx context.Context, token *oauth2.Token) error {
|
||||
params := url.Values{}
|
||||
params.Add("access_token", token.AccessToken)
|
||||
|
||||
err := httputil.Client(ctx, http.MethodPost, p.RevokeURL, version.UserAgent(), nil, params, nil)
|
||||
if err != nil && err != httputil.ErrTokenRevoked {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -5,18 +5,14 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
admin "google.golang.org/api/admin/directory/v1"
|
||||
|
||||
"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 defaultGoogleProviderURL = "https://accounts.google.com"
|
||||
|
@ -25,8 +21,6 @@ const defaultGoogleProviderURL = "https://accounts.google.com"
|
|||
type GoogleProvider struct {
|
||||
*Provider
|
||||
|
||||
RevokeURL string `json:"revocation_endpoint"`
|
||||
|
||||
apiClient *admin.Service
|
||||
}
|
||||
|
||||
|
@ -95,19 +89,6 @@ func NewGoogleProvider(p *Provider) (*GoogleProvider, error) {
|
|||
return gp, nil
|
||||
}
|
||||
|
||||
// Revoke revokes the access token a given session state.
|
||||
//
|
||||
// https://developers.google.com/identity/protocols/OAuth2WebServer#tokenrevoke
|
||||
func (p *GoogleProvider) Revoke(ctx context.Context, token *oauth2.Token) error {
|
||||
params := url.Values{}
|
||||
params.Add("token", token.AccessToken)
|
||||
err := httputil.Client(ctx, http.MethodPost, p.RevokeURL, version.UserAgent(), nil, params, nil)
|
||||
if err != nil && err != httputil.ErrTokenRevoked {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSignInURL returns a URL to OAuth 2.0 provider's consent page that asks for permissions for
|
||||
// the required scopes explicitly.
|
||||
// Google requires an additional access scope for offline access which is a requirement for any
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
|
@ -26,8 +25,6 @@ const defaultAzureGroupURL = "https://graph.microsoft.com/v1.0/me/memberOf"
|
|||
// AzureProvider is an implementation of the Provider interface
|
||||
type AzureProvider struct {
|
||||
*Provider
|
||||
// non-standard oidc fields
|
||||
RevokeURL string `json:"end_session_endpoint"`
|
||||
}
|
||||
|
||||
// NewAzureProvider returns a new AzureProvider and sets the provider url endpoints.
|
||||
|
@ -64,18 +61,6 @@ func NewAzureProvider(p *Provider) (*AzureProvider, error) {
|
|||
return azureProvider, nil
|
||||
}
|
||||
|
||||
// Revoke revokes the access token a given session state.
|
||||
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
|
||||
func (p *AzureProvider) Revoke(ctx context.Context, token *oauth2.Token) error {
|
||||
params := url.Values{}
|
||||
params.Add("token", token.AccessToken)
|
||||
err := httputil.Client(ctx, http.MethodPost, p.RevokeURL, version.UserAgent(), nil, params, nil)
|
||||
if err != nil && err != httputil.ErrTokenRevoked {
|
||||
return err
|
||||
}
|
||||
return 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"))
|
||||
|
|
|
@ -2,6 +2,7 @@ package identity
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
|
@ -16,6 +17,8 @@ type MockProvider struct {
|
|||
RefreshError error
|
||||
RevokeError error
|
||||
GetSignInURLResponse string
|
||||
LogOutResponse url.URL
|
||||
LogOutError error
|
||||
}
|
||||
|
||||
// Authenticate is a mocked providers function.
|
||||
|
@ -35,3 +38,6 @@ func (mp MockProvider) Revoke(ctx context.Context, s *oauth2.Token) error {
|
|||
|
||||
// GetSignInURL is a mocked providers function.
|
||||
func (mp MockProvider) GetSignInURL(s string) string { return mp.GetSignInURLResponse }
|
||||
|
||||
// LogOut is a mocked providers function.
|
||||
func (mp MockProvider) LogOut() (*url.URL, error) { return &mp.LogOutResponse, mp.LogOutError }
|
||||
|
|
|
@ -22,8 +22,7 @@ import (
|
|||
type OktaProvider struct {
|
||||
*Provider
|
||||
|
||||
RevokeURL string `json:"revocation_endpoint"`
|
||||
userAPI *url.URL
|
||||
userAPI *url.URL
|
||||
}
|
||||
|
||||
// NewOktaProvider creates a new instance of Okta as an identity provider.
|
||||
|
@ -49,7 +48,6 @@ func NewOktaProvider(p *Provider) (*OktaProvider, error) {
|
|||
Scopes: p.Scopes,
|
||||
}
|
||||
|
||||
// okta supports a revocation endpoint
|
||||
oktaProvider := OktaProvider{Provider: p}
|
||||
if err := p.provider.Claims(&oktaProvider); err != nil {
|
||||
return nil, err
|
||||
|
@ -70,21 +68,6 @@ func NewOktaProvider(p *Provider) (*OktaProvider, error) {
|
|||
return &oktaProvider, nil
|
||||
}
|
||||
|
||||
// Revoke revokes the access token a given session state.
|
||||
// https://developer.okta.com/docs/api/resources/oidc#revoke
|
||||
func (p *OktaProvider) Revoke(ctx context.Context, token *oauth2.Token) error {
|
||||
params := url.Values{}
|
||||
params.Add("client_id", p.ClientID)
|
||||
params.Add("client_secret", p.ClientSecret)
|
||||
params.Add("token", token.AccessToken)
|
||||
params.Add("token_type_hint", "refresh_token")
|
||||
err := httputil.Client(ctx, http.MethodPost, p.RevokeURL, version.UserAgent(), nil, params, nil)
|
||||
if err != nil && err != httputil.ErrTokenRevoked {
|
||||
return err
|
||||
}
|
||||
return 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) {
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
|
@ -23,9 +22,6 @@ const defaultOneloginGroupURL = "https://openid-connect.onelogin.com/oidc/me"
|
|||
// of an authorization identity provider.
|
||||
type OneLoginProvider struct {
|
||||
*Provider
|
||||
|
||||
// non-standard oidc fields
|
||||
RevokeURL string `json:"revocation_endpoint"`
|
||||
}
|
||||
|
||||
// NewOneLoginProvider creates a new instance of an OpenID Connect provider.
|
||||
|
@ -62,21 +58,6 @@ func NewOneLoginProvider(p *Provider) (*OneLoginProvider, error) {
|
|||
return &olProvider, nil
|
||||
}
|
||||
|
||||
// Revoke revokes the access token a given session state.
|
||||
// https://developers.onelogin.com/openid-connect/api/revoke-session
|
||||
func (p *OneLoginProvider) Revoke(ctx context.Context, token *oauth2.Token) error {
|
||||
params := url.Values{}
|
||||
params.Add("client_id", p.ClientID)
|
||||
params.Add("client_secret", p.ClientSecret)
|
||||
params.Add("token", token.AccessToken)
|
||||
params.Add("token_type_hint", "access_token")
|
||||
err := httputil.Client(ctx, http.MethodPost, p.RevokeURL, version.UserAgent(), nil, params, nil)
|
||||
if err != nil && err != httputil.ErrTokenRevoked {
|
||||
return fmt.Errorf("identity/onelogin: revocation error %w", err)
|
||||
}
|
||||
return 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) {
|
||||
|
|
|
@ -6,9 +6,13 @@ 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"
|
||||
|
@ -41,6 +45,7 @@ type Authenticator interface {
|
|||
Refresh(context.Context, *sessions.State) (*sessions.State, error)
|
||||
Revoke(context.Context, *oauth2.Token) error
|
||||
GetSignInURL(state string) string
|
||||
LogOut() (*url.URL, error)
|
||||
}
|
||||
|
||||
// New returns a new identity provider based on its name.
|
||||
|
@ -94,6 +99,22 @@ type Provider struct {
|
|||
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
|
||||
|
@ -124,14 +145,7 @@ func (p *Provider) Authenticate(ctx context.Context, code string) (*sessions.Sta
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// check if provider has info endpoint, try to hit that and gather more info
|
||||
// especially useful if initial request did not an contain email, or subject
|
||||
// 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 != "" {
|
||||
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)
|
||||
|
@ -150,7 +164,7 @@ func (p *Provider) Authenticate(ctx context.Context, code string) (*sessions.Sta
|
|||
return s, nil
|
||||
}
|
||||
|
||||
// Refresh renews a user's session using an oidc refresh token withoutreprompting the user.
|
||||
// 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) {
|
||||
|
@ -190,8 +204,39 @@ func (p *Provider) IdentityFromToken(ctx context.Context, t *oauth2.Token) (*oid
|
|||
return p.verifier.Verify(ctx, rawIDToken)
|
||||
}
|
||||
|
||||
// Revoke enables a user to revoke her token. If the identity provider supports revocation
|
||||
// the endpoint is available, otherwise an error is thrown.
|
||||
// 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 {
|
||||
return ErrRevokeNotImplemented
|
||||
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
Add a link
Reference in a new issue