mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-09 14:22:40 +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
|
// SignOut signs the user out and attempts to revoke the user's identity session
|
||||||
// Handles both GET and POST.
|
// Handles both GET and POST.
|
||||||
func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) error {
|
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())
|
jwt, err := sessions.FromContext(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httputil.NewError(http.StatusBadRequest, err)
|
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)
|
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)
|
err = a.provider.Revoke(r.Context(), s.AccessToken)
|
||||||
if errors.Is(err, identity.ErrRevokeNotImplemented) {
|
if err != nil && !errors.Is(err, identity.ErrRevokeNotImplemented) {
|
||||||
log.FromRequest(r).Warn().Err(err).Msg("authenticate: revoke not implemented")
|
|
||||||
} else if err != nil {
|
|
||||||
return httputil.NewError(http.StatusBadRequest, err)
|
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 {
|
if err != nil {
|
||||||
return httputil.NewError(http.StatusBadRequest, err)
|
return httputil.NewError(http.StatusBadRequest, err)
|
||||||
|
|
||||||
}
|
}
|
||||||
httputil.Redirect(w, r, redirectURL.String(), http.StatusFound)
|
httputil.Redirect(w, r, redirectURL.String(), http.StatusFound)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -189,10 +189,10 @@ func TestAuthenticate_SignOut(t *testing.T) {
|
||||||
wantCode int
|
wantCode int
|
||||||
wantBody string
|
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"},
|
{"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"},
|
{"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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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"`
|
TLSCustomCAFile string `mapstructure:"tls_custom_ca_file" yaml:"tls_custom_ca_file,omitempty"`
|
||||||
RootCAs *x509.CertPool `yaml:",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.
|
// host.
|
||||||
TLSClientCert string `mapstructure:"tls_client_cert" yaml:"tls_client_cert,omitempty"`
|
TLSClientCert string `mapstructure:"tls_client_cert" yaml:"tls_client_cert,omitempty"`
|
||||||
TLSClientKey string `mapstructure:"tls_client_key" yaml:"tls_client_key,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
|
// ErrRevokeNotImplemented error type when Revoke method is not implemented
|
||||||
// by an identity provider
|
// by an identity provider
|
||||||
var ErrRevokeNotImplemented = errors.New("internal/identity: revoke not implemented")
|
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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
|
@ -19,11 +21,11 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultGitHubProviderURL = "https://github.com"
|
defaultGitHubProviderURL = "https://github.com"
|
||||||
githubAuthURL = "/login/oauth/authorize"
|
githubAPIURL = "https://api.github.com"
|
||||||
githubUserURL = "https://api.github.com/user"
|
userPath = "/user"
|
||||||
githubUserTeamURL = "https://api.github.com/user/teams"
|
teamPath = "/user/teams"
|
||||||
githubRevokeURL = "https://github.com/oauth/revoke"
|
revokePath = "/applications/%s/grant"
|
||||||
githubUserEmailURL = "https://api.github.com/user/emails"
|
emailPath = "/user/emails"
|
||||||
|
|
||||||
// since github doesn't implement oidc, we need this to refresh the user session
|
// since github doesn't implement oidc, we need this to refresh the user session
|
||||||
refreshDeadline = time.Minute * 60
|
refreshDeadline = time.Minute * 60
|
||||||
|
@ -33,22 +35,11 @@ const (
|
||||||
type GitHubProvider struct {
|
type GitHubProvider struct {
|
||||||
*Provider
|
*Provider
|
||||||
|
|
||||||
authURL string
|
|
||||||
tokenURL string
|
|
||||||
userEndpoint string
|
userEndpoint string
|
||||||
|
|
||||||
RevokeURL string `json:"revocation_endpoint"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGitHubProvider returns a new GitHubProvider.
|
// NewGitHubProvider returns a new GitHubProvider.
|
||||||
func NewGitHubProvider(p *Provider) (*GitHubProvider, error) {
|
func NewGitHubProvider(p *Provider) (*GitHubProvider, error) {
|
||||||
gp := &GitHubProvider{
|
|
||||||
authURL: defaultGitHubProviderURL + githubAuthURL,
|
|
||||||
tokenURL: defaultGitHubProviderURL + "/login/oauth/access_token",
|
|
||||||
userEndpoint: githubUserURL,
|
|
||||||
RevokeURL: githubRevokeURL,
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.ProviderURL == "" {
|
if p.ProviderURL == "" {
|
||||||
p.ProviderURL = defaultGitHubProviderURL
|
p.ProviderURL = defaultGitHubProviderURL
|
||||||
}
|
}
|
||||||
|
@ -61,13 +52,16 @@ func NewGitHubProvider(p *Provider) (*GitHubProvider, error) {
|
||||||
ClientID: p.ClientID,
|
ClientID: p.ClientID,
|
||||||
ClientSecret: p.ClientSecret,
|
ClientSecret: p.ClientSecret,
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
AuthURL: gp.authURL,
|
AuthURL: p.ProviderURL + "/login/oauth/authorize",
|
||||||
TokenURL: gp.tokenURL,
|
TokenURL: p.ProviderURL + "/login/oauth/access_token",
|
||||||
},
|
},
|
||||||
RedirectURL: p.RedirectURL.String(),
|
RedirectURL: p.RedirectURL.String(),
|
||||||
Scopes: p.Scopes,
|
Scopes: p.Scopes,
|
||||||
}
|
}
|
||||||
gp.Provider = p
|
gp := &GitHubProvider{
|
||||||
|
Provider: p,
|
||||||
|
userEndpoint: githubAPIURL + userPath,
|
||||||
|
}
|
||||||
|
|
||||||
return gp, nil
|
return gp, nil
|
||||||
}
|
}
|
||||||
|
@ -107,7 +101,7 @@ func (p *GitHubProvider) updateSessionState(ctx context.Context, s *sessions.Sta
|
||||||
|
|
||||||
err := p.userInfo(ctx, accessToken, s)
|
err := p.userInfo(ctx, accessToken, s)
|
||||||
if err != nil {
|
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)
|
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)}
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -182,7 +177,8 @@ func (p *GitHubProvider) userEmail(ctx context.Context, at string, s *sessions.S
|
||||||
Visibility string `json:"visibility"`
|
Visibility string `json:"visibility"`
|
||||||
}
|
}
|
||||||
headers := map[string]string{"Authorization": fmt.Sprintf("token %s", at)}
|
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 {
|
if err != nil {
|
||||||
return err
|
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))
|
s.Expiry = jwt.NewNumericDate(time.Now().Add(refreshDeadline))
|
||||||
return nil
|
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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
oidc "github.com/coreos/go-oidc"
|
oidc "github.com/coreos/go-oidc"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
@ -25,7 +24,6 @@ const (
|
||||||
// GitLabProvider is an implementation of the OAuth Provider
|
// GitLabProvider is an implementation of the OAuth Provider
|
||||||
type GitLabProvider struct {
|
type GitLabProvider struct {
|
||||||
*Provider
|
*Provider
|
||||||
RevokeURL string `json:"revocation_endpoint"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGitLabProvider returns a new GitLabProvider.
|
// NewGitLabProvider returns a new GitLabProvider.
|
||||||
|
@ -55,9 +53,7 @@ func NewGitLabProvider(p *Provider) (*GitLabProvider, error) {
|
||||||
RedirectURL: p.RedirectURL.String(),
|
RedirectURL: p.RedirectURL.String(),
|
||||||
Scopes: p.Scopes,
|
Scopes: p.Scopes,
|
||||||
}
|
}
|
||||||
gp := &GitLabProvider{
|
gp := &GitLabProvider{Provider: p}
|
||||||
Provider: p,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.provider.Claims(&gp); err != nil {
|
if err := p.provider.Claims(&gp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -103,17 +99,3 @@ func (p *GitLabProvider) UserGroups(ctx context.Context, s *sessions.State) ([]s
|
||||||
|
|
||||||
return groups, nil
|
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/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
oidc "github.com/coreos/go-oidc"
|
oidc "github.com/coreos/go-oidc"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/google"
|
"golang.org/x/oauth2/google"
|
||||||
admin "google.golang.org/api/admin/directory/v1"
|
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/log"
|
||||||
"github.com/pomerium/pomerium/internal/sessions"
|
"github.com/pomerium/pomerium/internal/sessions"
|
||||||
"github.com/pomerium/pomerium/internal/version"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultGoogleProviderURL = "https://accounts.google.com"
|
const defaultGoogleProviderURL = "https://accounts.google.com"
|
||||||
|
@ -25,8 +21,6 @@ const defaultGoogleProviderURL = "https://accounts.google.com"
|
||||||
type GoogleProvider struct {
|
type GoogleProvider struct {
|
||||||
*Provider
|
*Provider
|
||||||
|
|
||||||
RevokeURL string `json:"revocation_endpoint"`
|
|
||||||
|
|
||||||
apiClient *admin.Service
|
apiClient *admin.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,19 +89,6 @@ func NewGoogleProvider(p *Provider) (*GoogleProvider, error) {
|
||||||
return gp, nil
|
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
|
// GetSignInURL returns a URL to OAuth 2.0 provider's consent page that asks for permissions for
|
||||||
// the required scopes explicitly.
|
// the required scopes explicitly.
|
||||||
// Google requires an additional access scope for offline access which is a requirement for any
|
// Google requires an additional access scope for offline access which is a requirement for any
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
oidc "github.com/coreos/go-oidc"
|
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
|
// AzureProvider is an implementation of the Provider interface
|
||||||
type AzureProvider struct {
|
type AzureProvider struct {
|
||||||
*Provider
|
*Provider
|
||||||
// non-standard oidc fields
|
|
||||||
RevokeURL string `json:"end_session_endpoint"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAzureProvider returns a new AzureProvider and sets the provider url endpoints.
|
// NewAzureProvider returns a new AzureProvider and sets the provider url endpoints.
|
||||||
|
@ -64,18 +61,6 @@ func NewAzureProvider(p *Provider) (*AzureProvider, error) {
|
||||||
return azureProvider, nil
|
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
|
// GetSignInURL returns the sign in url with typical oauth parameters
|
||||||
func (p *AzureProvider) GetSignInURL(state string) string {
|
func (p *AzureProvider) GetSignInURL(state string) string {
|
||||||
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "select_account"))
|
return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "select_account"))
|
||||||
|
|
|
@ -2,6 +2,7 @@ package identity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
@ -16,6 +17,8 @@ type MockProvider struct {
|
||||||
RefreshError error
|
RefreshError error
|
||||||
RevokeError error
|
RevokeError error
|
||||||
GetSignInURLResponse string
|
GetSignInURLResponse string
|
||||||
|
LogOutResponse url.URL
|
||||||
|
LogOutError error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate is a mocked providers function.
|
// 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.
|
// GetSignInURL is a mocked providers function.
|
||||||
func (mp MockProvider) GetSignInURL(s string) string { return mp.GetSignInURLResponse }
|
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,7 +22,6 @@ import (
|
||||||
type OktaProvider struct {
|
type OktaProvider struct {
|
||||||
*Provider
|
*Provider
|
||||||
|
|
||||||
RevokeURL string `json:"revocation_endpoint"`
|
|
||||||
userAPI *url.URL
|
userAPI *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +48,6 @@ func NewOktaProvider(p *Provider) (*OktaProvider, error) {
|
||||||
Scopes: p.Scopes,
|
Scopes: p.Scopes,
|
||||||
}
|
}
|
||||||
|
|
||||||
// okta supports a revocation endpoint
|
|
||||||
oktaProvider := OktaProvider{Provider: p}
|
oktaProvider := OktaProvider{Provider: p}
|
||||||
if err := p.provider.Claims(&oktaProvider); err != nil {
|
if err := p.provider.Claims(&oktaProvider); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -70,21 +68,6 @@ func NewOktaProvider(p *Provider) (*OktaProvider, error) {
|
||||||
return &oktaProvider, nil
|
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
|
// UserGroups fetches the groups of which the user is a member
|
||||||
// https://developer.okta.com/docs/reference/api/users/#get-user-s-groups
|
// https://developer.okta.com/docs/reference/api/users/#get-user-s-groups
|
||||||
func (p *OktaProvider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
func (p *OktaProvider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
oidc "github.com/coreos/go-oidc"
|
oidc "github.com/coreos/go-oidc"
|
||||||
|
@ -23,9 +22,6 @@ const defaultOneloginGroupURL = "https://openid-connect.onelogin.com/oidc/me"
|
||||||
// of an authorization identity provider.
|
// of an authorization identity provider.
|
||||||
type OneLoginProvider struct {
|
type OneLoginProvider struct {
|
||||||
*Provider
|
*Provider
|
||||||
|
|
||||||
// non-standard oidc fields
|
|
||||||
RevokeURL string `json:"revocation_endpoint"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOneLoginProvider creates a new instance of an OpenID Connect provider.
|
// NewOneLoginProvider creates a new instance of an OpenID Connect provider.
|
||||||
|
@ -62,21 +58,6 @@ func NewOneLoginProvider(p *Provider) (*OneLoginProvider, error) {
|
||||||
return &olProvider, nil
|
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.
|
// UserGroups returns a slice of group names a given user is in.
|
||||||
// https://developers.onelogin.com/openid-connect/api/user-info
|
// https://developers.onelogin.com/openid-connect/api/user-info
|
||||||
func (p *OneLoginProvider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
func (p *OneLoginProvider) UserGroups(ctx context.Context, s *sessions.State) ([]string, error) {
|
||||||
|
|
|
@ -6,9 +6,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/internal/httputil"
|
||||||
"github.com/pomerium/pomerium/internal/sessions"
|
"github.com/pomerium/pomerium/internal/sessions"
|
||||||
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
|
"github.com/pomerium/pomerium/internal/version"
|
||||||
|
|
||||||
oidc "github.com/coreos/go-oidc"
|
oidc "github.com/coreos/go-oidc"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
@ -41,6 +45,7 @@ type Authenticator interface {
|
||||||
Refresh(context.Context, *sessions.State) (*sessions.State, error)
|
Refresh(context.Context, *sessions.State) (*sessions.State, error)
|
||||||
Revoke(context.Context, *oauth2.Token) error
|
Revoke(context.Context, *oauth2.Token) error
|
||||||
GetSignInURL(state string) string
|
GetSignInURL(state string) string
|
||||||
|
LogOut() (*url.URL, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new identity provider based on its name.
|
// New returns a new identity provider based on its name.
|
||||||
|
@ -94,6 +99,22 @@ type Provider struct {
|
||||||
provider *oidc.Provider
|
provider *oidc.Provider
|
||||||
verifier *oidc.IDTokenVerifier
|
verifier *oidc.IDTokenVerifier
|
||||||
oauth *oauth2.Config
|
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
|
// 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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if provider has info endpoint, try to hit that and gather more info
|
if err := p.provider.Claims(&p); err == nil && p.UserInfoURL != "" {
|
||||||
// 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 != "" {
|
|
||||||
userInfo, err := p.provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
|
userInfo, err := p.provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("internal/identity: could not retrieve user info %w", err)
|
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
|
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.
|
// Group membership is also refreshed.
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
|
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
|
||||||
func (p *Provider) Refresh(ctx context.Context, s *sessions.State) (*sessions.State, error) {
|
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)
|
return p.verifier.Verify(ctx, rawIDToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revoke enables a user to revoke her token. If the identity provider supports revocation
|
// Revoke enables a user to revoke her token. If the identity provider does not
|
||||||
// the endpoint is available, otherwise an error is thrown.
|
// support revocation an error is thrown.
|
||||||
|
//
|
||||||
|
// https://tools.ietf.org/html/rfc7009
|
||||||
func (p *Provider) Revoke(ctx context.Context, token *oauth2.Token) error {
|
func (p *Provider) Revoke(ctx context.Context, token *oauth2.Token) error {
|
||||||
|
if p.RevocationURL == "" {
|
||||||
return ErrRevokeNotImplemented
|
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