Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Appleid #3959

Merged
merged 7 commits into from
Feb 14, 2023
Merged

Appleid #3959

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions authenticate/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ func (a *Authenticate) Mount(r *mux.Router) {
csrf.FormValueName("state"), // rfc6749#section-10.12
csrf.CookieName(csrfKey),
csrf.FieldName(csrfKey),
csrf.SameSite(csrf.SameSiteLaxMode),
// csrf.SameSiteLaxMode will cause browsers to reset
// the session on POST. This breaks Appleid being able
// to verify the csrf token.
csrf.SameSite(csrf.SameSiteNoneMode),
csrf.ErrorHandler(httputil.HandlerFunc(httputil.CSRFFailureHandler)),
)(h)
})
Expand All @@ -66,7 +69,7 @@ func (a *Authenticate) Mount(r *mux.Router) {

r.Path("/robots.txt").HandlerFunc(a.RobotsTxt).Methods(http.MethodGet)
// Identity Provider (IdP) endpoints
r.Path("/oauth2/callback").Handler(httputil.HandlerFunc(a.OAuthCallback)).Methods(http.MethodGet)
r.Path("/oauth2/callback").Handler(httputil.HandlerFunc(a.OAuthCallback)).Methods(http.MethodGet, http.MethodPost)

a.mountDashboard(r)
}
Expand Down
200 changes: 200 additions & 0 deletions internal/identity/oauth/apple/apple.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Package apple implements OpenID Connect for apple
//
// https://www.pomerium.com/docs/identity-providers/apple
package apple

import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"

"golang.org/x/oauth2"

"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/identity/identity"
"github.com/pomerium/pomerium/internal/identity/oauth"
"github.com/pomerium/pomerium/internal/identity/oidc"
"github.com/pomerium/pomerium/internal/urlutil"
"github.com/pomerium/pomerium/internal/version"
)

// Name identifies the apple identity provider.
const Name = "apple"

var defaultScopes = []string{"name", "email"}
var defaultAuthCodeOptions = map[string]string{
"response_mode": "form_post",
}

const (
defaultProviderURL = "https://appleid.apple.com"
// ignore G101 linting issue as this is clearly a false positive
tokenURL = "/auth/token" //nolint: gosec
authURL = "/auth/authorize"
refreshDeadline = time.Minute * 60
revocationURL = "/auth/revoke"
)

// Provider is an Apple implementation of the Authenticator interface.
type Provider struct {
Oauth *oauth2.Config
}

// New instantiates an OpenID Connect (OIDC) provider for Apple.
func New(ctx context.Context, o *oauth.Options) (*Provider, error) {
p := Provider{}
if o.ProviderURL == "" {
o.ProviderURL = defaultProviderURL
}

if len(o.Scopes) == 0 {
o.Scopes = defaultScopes
}

// Apple expects the AuthStyle to use Params instead of Headers
// So we have to do out own oauth2 config
p.Oauth = &oauth2.Config{
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
Scopes: o.Scopes,
RedirectURL: o.RedirectURL.String(),
Endpoint: oauth2.Endpoint{
AuthURL: urlutil.Join(o.ProviderURL, authURL),
TokenURL: urlutil.Join(o.ProviderURL, tokenURL),
AuthStyle: oauth2.AuthStyleInParams,
},
}

return &p, nil
}

// Name returns the provider name.
func (p *Provider) Name() string {
return Name
}

// 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, error) {
opts := []oauth2.AuthCodeOption{}
for k, v := range defaultAuthCodeOptions {
opts = append(opts, oauth2.SetAuthURLParam(k, v))
}
authURL := p.Oauth.AuthCodeURL(state, opts...)

// Apple is very picky here and we need to use %20 instead of +
// in order for all Apples device to correctly detect and use
// native auth when available.
// authURL = strings.Replace(authURL, "response_type=code", "response_type=code%20id_token", -1)
authURL = strings.Replace(authURL, "scope=name+email", "scope=name%20email", -1)

return authURL, nil
}

// 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, v identity.State) (*oauth2.Token, error) {
oauth2Token, err := p.Oauth.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("identity/oidc: token exchange failed: %w", err)
}

err = p.UpdateUserInfo(ctx, oauth2Token, v)
if err != nil {
return nil, err
}

return oauth2Token, nil
}

// LogOut is not implemented by Apple.
func (p *Provider) LogOut() (*url.URL, error) {
return nil, oidc.ErrSignoutNotImplemented
}

// Refresh renews a user's session.
func (p *Provider) Refresh(ctx context.Context, t *oauth2.Token, v identity.State) (*oauth2.Token, error) {
if t == nil {
return nil, oidc.ErrMissingAccessToken
}
if t.RefreshToken == "" {
return nil, oidc.ErrMissingRefreshToken
}

newToken, err := p.Oauth.TokenSource(ctx, t).Token()
if err != nil {
return nil, fmt.Errorf("identity/oidc: refresh failed: %w", err)
}

// Many identity providers _will not_ return `id_token` on refresh
// https://github.com/FusionAuth/fusionauth-issues/issues/110#issuecomment-481526544
err = p.UpdateUserInfo(ctx, newToken, v)
if err != nil {
return nil, err
}

return newToken, nil
}

// Revoke method will remove all the Apple grants the user
// gave pomerium application during authorization.
//
// https://developer.github.com/v3/apps/oauth_applications/#delete-an-app-authorization
func (p *Provider) Revoke(ctx context.Context, t *oauth2.Token) error {
if t == nil {
return oidc.ErrMissingAccessToken
}

params := url.Values{}
params.Add("token", t.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.Do(ctx, http.MethodPost, revocationURL, version.UserAgent(), nil, params, nil)
if err != nil && errors.Is(err, httputil.ErrTokenRevoked) {
return fmt.Errorf("internal/oidc: unexpected revoke error: %w", err)
}

return nil
}

// UpdateUserInfo will get the user information from Apple and also retrieve the user's team(s)
//
// https://developer.github.com/v3/users/#get-the-authenticated-user
func (p *Provider) UpdateUserInfo(ctx context.Context, t *oauth2.Token, v interface{}) error {
rawIDToken, ok := t.Extra("id_token").(string)
if !ok {
return oidc.ErrMissingIDToken
}

v.(identity.State).SetRawIDToken(rawIDToken)

attributes := strings.Split(rawIDToken, ".")[1]

rawDecodedText, err := base64.RawStdEncoding.DecodeString(attributes)
if err != nil {
return err
}

err = json.Unmarshal(rawDecodedText, v)
if err != nil {
return err
}

return nil
}
3 changes: 3 additions & 0 deletions internal/identity/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/pomerium/pomerium/internal/identity/identity"
"github.com/pomerium/pomerium/internal/identity/oauth"
"github.com/pomerium/pomerium/internal/identity/oauth/apple"
"github.com/pomerium/pomerium/internal/identity/oauth/github"
"github.com/pomerium/pomerium/internal/identity/oidc"
"github.com/pomerium/pomerium/internal/identity/oidc/auth0"
Expand All @@ -37,6 +38,8 @@ type Authenticator interface {
func NewAuthenticator(o oauth.Options) (a Authenticator, err error) {
ctx := context.Background()
switch o.ProviderName {
case apple.Name:
a, err = apple.New(ctx, &o)
case auth0.Name:
a, err = auth0.New(ctx, &o)
case azure.Name:
Expand Down