This commit is contained in:
Yangjunwei Liang 2026-08-13 01:18:40 +08:00 committed by GitHub
commit a7c1d8a706
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 702 additions and 4 deletions

View File

@ -8,6 +8,8 @@
## Changes since v7.15.3
- [#3293](https://github.com/oauth2-proxy/oauth2-proxy/pull/3293) feat: add Apple Sign in with Apple provider (@LYJW131)
# V7.15.3
## Release Highlights

View File

@ -312,6 +312,19 @@ They may change between releases without notice.
| `metricsServer` | _[Server](#server)_ | MetricsServer is used to configure the HTTP(S) server for metrics.<br/>You may choose to run both HTTP and HTTPS servers simultaneously.<br/>This can be done by setting the BindAddress and the SecureBindAddress simultaneously.<br/>To use the secure server you must configure a TLS certificate and key. |
| `providers` | _[Providers](#providers)_ | Providers is used to configure your provider. **Multiple-providers is not<br/>yet working.** [This feature is tracked in<br/>#925](https://github.com/oauth2-proxy/oauth2-proxy/issues/926) |
### AppleOptions
(**Appears on:** [Provider](#provider))
| Field | Type | Description |
| ----- | ---- | ----------- |
| `teamID` | _string_ | TeamID is the 10-character Apple Developer Team ID |
| `keyID` | _string_ | KeyID is the 10-character identifier for the private key |
| `privateKey` | _string_ | PrivateKey is the PEM-encoded ES256 private key content (from .p8 file) |
| `privateKeyFile` | _string_ | PrivateKeyFile is the path to the .p8 private key file |
### AzureOptions
(**Appears on:** [Provider](#provider))
@ -552,6 +565,7 @@ character.
| `audienceClaims` | _[]string_ | AudienceClaim allows to define any claim that is verified against the client id<br/>By default `aud` claim is used for verification. |
| `extraAudiences` | _[]string_ | ExtraAudiences is a list of additional audiences that are allowed<br/>to pass verification in addition to the client id. |
| `enabledSigningAlgs` | _[]string_ | EnabledSigningAlgs is a list of allowed JWT signing algorithms.<br/>When discovery is enabled, the effective set is the intersection<br/>between this list and the provider's discovered supported algorithms.<br/>By default `RS256` is used if nothing has been discovered or specified. |
| `authStyle` | _string_ | AuthStyle specifies how the endpoint wants the client ID & client secret sent.<br/>Possible values are:<br/> - "inParams" (or "params"): sends credentials in the POST body as application/x-www-form-urlencoded parameters<br/> - "inHeader" (or "header"): sends credentials using HTTP Basic Authorization<br/> - "" (empty, default): auto-detect by trying both ways<br/>Some providers like Apple require "inParams". |
### Provider
@ -574,6 +588,7 @@ Provider holds all configuration for a single provider
| `googleConfig` | _[GoogleOptions](#googleoptions)_ | GoogleConfig holds all configurations for Google provider. |
| `oidcConfig` | _[OIDCOptions](#oidcoptions)_ | OIDCConfig holds all configurations for OIDC provider<br/>or providers utilize OIDC configurations. |
| `loginGovConfig` | _[LoginGovOptions](#logingovoptions)_ | LoginGovConfig holds all configurations for LoginGov provider. |
| `appleConfig` | _[AppleOptions](#appleoptions)_ | AppleConfig holds all configurations for Apple provider. |
| `id` | _string_ | ID should be a unique identifier for the provider.<br/>This value is required for all providers. |
| `provider` | _[ProviderType](#providertype)_ | Type is the OAuth provider<br/>must be set from the supported providers group,<br/>otherwise 'Google' is set as default |
| `name` | _string_ | Name is the providers display name<br/>if set, it will be shown to the users in the login page. |

View File

@ -0,0 +1,138 @@
---
id: apple
title: Apple
---
[Apple Sign in with Apple](https://developer.apple.com/documentation/sign_in_with_apple) is an OIDC provider that supports authentication using Apple ID.
## Prerequisites
1. You need an Apple Developer account
2. Register an App ID in the [Apple Developer Portal](https://developer.apple.com/account/resources/identifiers/list)
3. Create a Service ID for Sign in with Apple
4. Create a private key for Sign in with Apple
## Configuration
### Step 1: Create an App ID
1. Go to [Identifiers](https://developer.apple.com/account/resources/identifiers/list) in the Apple Developer Portal
2. Click the `+` button to create a new identifier
3. Select "App IDs" and continue
4. Fill in the description and Bundle ID
5. Under Capabilities, enable "Sign in with Apple"
6. Click "Continue" and then "Register"
### Step 2: Create a Service ID
1. Go to [Identifiers](https://developer.apple.com/account/resources/identifiers/list)
2. Click the `+` button to create a new identifier
3. Select "Services IDs" and continue
4. Fill in the description and identifier (this will be your `client-id`)
5. Enable "Sign in with Apple" and click "Configure"
6. Select your Primary App ID
7. Add your domain and return URL (e.g., `https://your-domain.com/oauth2/callback`)
8. Click "Continue" and then "Register"
### Step 3: Create a Private Key
1. Go to [Keys](https://developer.apple.com/account/resources/authkeys/list)
2. Click the `+` button to create a new key
3. Enter a key name and enable "Sign in with Apple"
4. Click "Configure" and select your Primary App ID
5. Click "Continue" and then "Register"
6. Download the `.p8` private key file (you can only download it once!)
7. Note the Key ID (you'll need this for configuration)
### Step 4: Get Your Team ID
Your Team ID can be found in the top right corner of the Apple Developer Portal, or in [Membership Details](https://developer.apple.com/account/#!/membership).
## Usage
:::note
The Apple provider is only configurable via AlphaConfig.
:::
### Configuration Options
| Option | Type | Description |
|--------|------|-------------|
| `teamID` | string | Your 10-character Apple Developer Team ID |
| `keyID` | string | The 10-character Key ID of your private key |
| `privateKeyFile` | string | Path to the `.p8` private key file |
| `privateKey` | string | The private key content directly (alternative to file) |
**Note:** You must provide either `privateKeyFile` or `privateKey`, but not both.
### Example
```yaml
providers:
- id: apple
provider: apple
clientID: com.example.yourservice
appleConfig:
teamID: TEAM123456
keyID: KEY1234567
privateKeyFile: /path/to/AuthKey_KEY1234567.p8
```
Or with the private key content directly:
```yaml
providers:
- id: apple
provider: apple
clientID: com.example.yourservice
appleConfig:
teamID: TEAM123456
keyID: KEY1234567
privateKey: |
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg...
-----END PRIVATE KEY-----
```
## How It Works
Apple Sign in with Apple has some unique requirements compared to standard OIDC providers:
1. **Dynamic Client Secret**: Apple requires the `client_secret` to be a JWT signed with your private key (ES256). OAuth2 Proxy automatically generates this JWT for each token request.
2. **Form Post Response Mode**: Apple returns the authorization code via form POST rather than query parameters. OAuth2 Proxy handles this automatically.
3. **Limited User Information**: Apple only provides the user's email (and optionally name) on the first authentication. Subsequent authentications may not include the name.
## Restricting Access
Like other providers, you can restrict access using:
- `email_domains` to allow only specific email domains
- `authenticated_emails_file` to allow only specific email addresses
Example:
```yaml
providers:
- id: apple
provider: apple
clientID: com.example.yourservice
appleConfig:
teamID: TEAM123456
keyID: KEY1234567
privateKeyFile: /path/to/key.p8
upstreamConfig:
upstreams:
- id: backend
path: /
uri: http://localhost:3000/
injectRequestHeaders:
- name: X-Forwarded-Email
values:
- claim: email
emailDomains:
- yourcompany.com
```

View File

@ -9,6 +9,7 @@ with Redirect URI(s) for the domain you intend to run `oauth2-proxy` on.
Valid providers are :
- [ADFS](adfs.md)
- [Apple](apple.md)
- [Bitbucket](bitbucket.md)
- [Cidaas](cidaas.md)
- [CiscoDuo](cisco_duo.md)

View File

@ -93,6 +93,8 @@ type Provider struct {
OIDCConfig OIDCOptions `yaml:"oidcConfig,omitempty"`
// LoginGovConfig holds all configurations for LoginGov provider.
LoginGovConfig LoginGovOptions `yaml:"loginGovConfig,omitempty"`
// AppleConfig holds all configurations for Apple provider.
AppleConfig AppleOptions `yaml:"appleConfig,omitempty"`
// ID should be a unique identifier for the provider.
// This value is required for all providers.
@ -198,6 +200,9 @@ const (
// SourceHutProvider is the provider type for SourceHut
SourceHutProvider ProviderType = "sourcehut"
// AppleProvider is the provider type for Apple Sign in with Apple
AppleProvider ProviderType = "apple"
)
type KeycloakOptions struct {
@ -326,6 +331,13 @@ type OIDCOptions struct {
// between this list and the provider's discovered supported algorithms.
// By default `RS256` is used if nothing has been discovered or specified.
EnabledSigningAlgs []string `yaml:"enabledSigningAlgs,omitempty"`
// AuthStyle specifies how the endpoint wants the client ID & client secret sent.
// Possible values are:
// - "inParams" (or "params"): sends credentials in the POST body as application/x-www-form-urlencoded parameters
// - "inHeader" (or "header"): sends credentials using HTTP Basic Authorization
// - "" (empty, default): auto-detect by trying both ways
// Some providers like Apple require "inParams".
AuthStyle string `yaml:"authStyle,omitempty"`
}
type LoginGovOptions struct {
@ -337,6 +349,17 @@ type LoginGovOptions struct {
PubJWKURL string `yaml:"pubjwkURL,omitempty"`
}
type AppleOptions struct {
// TeamID is the 10-character Apple Developer Team ID
TeamID string `yaml:"teamID,omitempty"`
// KeyID is the 10-character identifier for the private key
KeyID string `yaml:"keyID,omitempty"`
// PrivateKey is the PEM-encoded ES256 private key content (from .p8 file)
PrivateKey string `yaml:"privateKey,omitempty"`
// PrivateKeyFile is the path to the .p8 private key file
PrivateKeyFile string `yaml:"privateKeyFile,omitempty"`
}
// Legacy default providers configuration
func providerDefaults() Providers {
providers := Providers{

View File

@ -106,6 +106,11 @@ func providerRequiresClientSecret(provider options.Provider) bool {
return false
}
// Apple uses a private key to dynamically generate client_secret JWTs
if provider.Type == options.AppleProvider {
return false
}
return true
}

169
providers/apple.go Normal file
View File

@ -0,0 +1,169 @@
package providers
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net/url"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"golang.org/x/oauth2"
)
const (
appleProviderName = "Apple"
appleDefaultScope = "openid email name"
appleAudience = "https://appleid.apple.com"
)
var (
appleDefaultLoginURL = &url.URL{
Scheme: "https",
Host: "appleid.apple.com",
Path: "/auth/authorize",
}
appleDefaultRedeemURL = &url.URL{
Scheme: "https",
Host: "appleid.apple.com",
Path: "/auth/token",
}
)
// AppleProvider represents the Apple Sign in with Apple OIDC provider
// This provider is only configurable via AlphaConfig.
// See: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api
type AppleProvider struct {
*OIDCProvider
TeamID string
KeyID string
PrivateKey *ecdsa.PrivateKey
}
var _ Provider = (*AppleProvider)(nil)
// NewAppleProvider creates a new AppleProvider
func NewAppleProvider(p *ProviderData, appleOpts options.AppleOptions, oidcOpts options.OIDCOptions) (*AppleProvider, error) {
p.setProviderDefaults(providerDefaults{
name: appleProviderName,
loginURL: appleDefaultLoginURL,
redeemURL: appleDefaultRedeemURL,
profileURL: nil,
validateURL: nil,
scope: appleDefaultScope,
})
// Apple returns the authorization code via form POST rather than query
// parameters, so default the response mode accordingly
if p.AuthRequestResponseMode == "" {
p.AuthRequestResponseMode = "form_post"
}
oidcProvider := NewOIDCProvider(p, oidcOpts)
// Apple requires the client credentials in the POST body
oidcProvider.AuthStyle = oauth2.AuthStyleInParams
provider := &AppleProvider{
OIDCProvider: oidcProvider,
TeamID: appleOpts.TeamID,
KeyID: appleOpts.KeyID,
}
if err := provider.initialize(appleOpts); err != nil {
return nil, fmt.Errorf("could not initialize Apple provider: %v", err)
}
// Set up dynamic client secret generation
// Apple requires client_secret to be a JWT signed with ES256
p.ClientSecretFunc = provider.generateClientSecret
return provider, nil
}
// initialize validates and configures the Apple provider with the private key
func (p *AppleProvider) initialize(opts options.AppleOptions) error {
if opts.TeamID == "" {
return errors.New("apple provider requires teamID")
}
if opts.KeyID == "" {
return errors.New("apple provider requires keyID")
}
// Private key can be supplied via config or file, but not both
switch {
case opts.PrivateKey != "" && opts.PrivateKeyFile != "":
return errors.New("cannot set both privateKey and privateKeyFile options")
case opts.PrivateKey == "" && opts.PrivateKeyFile == "":
return errors.New("apple provider requires a private key for signing JWTs")
case opts.PrivateKey != "":
key, err := parseECPrivateKey([]byte(opts.PrivateKey))
if err != nil {
return fmt.Errorf("could not parse EC private key: %v", err)
}
p.PrivateKey = key
case opts.PrivateKeyFile != "":
keyData, err := os.ReadFile(opts.PrivateKeyFile)
if err != nil {
return fmt.Errorf("could not read private key file %s: %v", opts.PrivateKeyFile, err)
}
key, err := parseECPrivateKey(keyData)
if err != nil {
return fmt.Errorf("could not parse private key from file %s: %v", opts.PrivateKeyFile, err)
}
p.PrivateKey = key
}
return nil
}
// parseECPrivateKey parses a PEM-encoded EC private key (Apple .p8 format)
// See: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
func parseECPrivateKey(keyData []byte) (*ecdsa.PrivateKey, error) {
// Apple .p8 files contain a PEM-encoded PKCS#8 private key
block, _ := pem.Decode(keyData)
if block == nil {
return nil, errors.New("failed to decode PEM block")
}
// Try PKCS#8 first (Apple's format)
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err == nil {
if ecKey, ok := key.(*ecdsa.PrivateKey); ok {
return ecKey, nil
}
return nil, errors.New("key is not an EC private key")
}
// Fall back to EC private key format
ecKey, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse EC private key: %v", err)
}
return ecKey, nil
}
// generateClientSecret creates a JWT client_secret for Apple token requests
// Apple requires the client_secret to be a JWT signed with ES256
// See: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
func (p *AppleProvider) generateClientSecret() (string, error) {
now := time.Now()
claims := &jwt.RegisteredClaims{
Issuer: p.TeamID,
Subject: p.ClientID,
Audience: jwt.ClaimStrings{appleAudience},
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(5 * time.Minute)), // Short-lived for security
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
token.Header["kid"] = p.KeyID
return token.SignedString(p.PrivateKey)
}

316
providers/apple_test.go Normal file
View File

@ -0,0 +1,316 @@
package providers
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/golang-jwt/jwt/v5"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/oidc"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/assert"
)
const (
appleTestIssuer = "https://appleid.apple.com"
appleTestClientID = "com.example.client"
)
// mockAppleJWKS implements oidc.KeySet for testing
type mockAppleJWKS struct{}
func (mockAppleJWKS) VerifySignature(_ context.Context, jwt string) ([]byte, error) {
decoded, err := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[1])
if err != nil {
return nil, fmt.Errorf("failed to decode JWT: %v", err)
}
return decoded, nil
}
func newAppleServer(body []byte) (*url.URL, *httptest.Server) {
s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
rw.Write(body)
}))
u, _ := url.Parse(s.URL)
return u, s
}
func generateTestECPrivateKey() (*ecdsa.PrivateKey, []byte, error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, err
}
keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return nil, nil, err
}
pemBlock := &pem.Block{
Type: "PRIVATE KEY",
Bytes: keyBytes,
}
return privateKey, pem.EncodeToMemory(pemBlock), nil
}
func newAppleProvider() (*AppleProvider, *ecdsa.PrivateKey, error) {
privKey, privKeyPEM, err := generateTestECPrivateKey()
if err != nil {
return nil, nil, err
}
verificationOptions := internaloidc.IDTokenVerificationOptions{
AudienceClaims: []string{"aud"},
ClientID: appleTestClientID,
}
p, err := NewAppleProvider(
&ProviderData{
ProviderName: "",
LoginURL: &url.URL{},
RedeemURL: &url.URL{},
ProfileURL: &url.URL{},
ValidateURL: &url.URL{},
Scope: "",
ClientID: appleTestClientID,
EmailClaim: "email",
UserClaim: "sub",
Verifier: internaloidc.NewVerifier(oidc.NewVerifier(
appleTestIssuer,
mockAppleJWKS{},
&oidc.Config{ClientID: appleTestClientID},
), verificationOptions),
},
options.AppleOptions{
TeamID: "TEAM123456",
KeyID: "KEY1234567",
PrivateKey: string(privKeyPEM),
},
options.OIDCOptions{},
)
if err != nil {
return nil, nil, err
}
return p, privKey, nil
}
func TestNewAppleProvider(t *testing.T) {
g := NewWithT(t)
_, privKeyPEM, err := generateTestECPrivateKey()
g.Expect(err).ToNot(HaveOccurred())
// Test that defaults are set when calling for a new provider
provider, err := NewAppleProvider(
&ProviderData{
ClientID: "com.example.client",
},
options.AppleOptions{
TeamID: "TEAM123456",
KeyID: "KEY1234567",
PrivateKey: string(privKeyPEM),
},
options.OIDCOptions{},
)
g.Expect(err).ToNot(HaveOccurred())
providerData := provider.Data()
g.Expect(providerData.ProviderName).To(Equal("Apple"))
g.Expect(providerData.LoginURL.String()).To(Equal("https://appleid.apple.com/auth/authorize"))
g.Expect(providerData.RedeemURL.String()).To(Equal("https://appleid.apple.com/auth/token"))
g.Expect(providerData.Scope).To(Equal("openid email name"))
}
func TestAppleProviderMissingTeamID(t *testing.T) {
_, privKeyPEM, err := generateTestECPrivateKey()
assert.NoError(t, err)
_, err = NewAppleProvider(
&ProviderData{},
options.AppleOptions{
KeyID: "KEY1234567",
PrivateKey: string(privKeyPEM),
},
options.OIDCOptions{},
)
assert.Error(t, err)
assert.Contains(t, err.Error(), "teamID")
}
func TestAppleProviderMissingKeyID(t *testing.T) {
_, privKeyPEM, err := generateTestECPrivateKey()
assert.NoError(t, err)
_, err = NewAppleProvider(
&ProviderData{},
options.AppleOptions{
TeamID: "TEAM123456",
PrivateKey: string(privKeyPEM),
},
options.OIDCOptions{},
)
assert.Error(t, err)
assert.Contains(t, err.Error(), "keyID")
}
func TestAppleProviderMissingPrivateKey(t *testing.T) {
_, err := NewAppleProvider(
&ProviderData{},
options.AppleOptions{
TeamID: "TEAM123456",
KeyID: "KEY1234567",
},
options.OIDCOptions{},
)
assert.Error(t, err)
assert.Contains(t, err.Error(), "private key")
}
func TestAppleProviderBothPrivateKeyOptions(t *testing.T) {
_, privKeyPEM, err := generateTestECPrivateKey()
assert.NoError(t, err)
_, err = NewAppleProvider(
&ProviderData{},
options.AppleOptions{
TeamID: "TEAM123456",
KeyID: "KEY1234567",
PrivateKey: string(privKeyPEM),
PrivateKeyFile: "/path/to/key.p8",
},
options.OIDCOptions{},
)
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot set both")
}
func TestAppleProviderGenerateClientSecret(t *testing.T) {
p, privKey, err := newAppleProvider()
assert.NoError(t, err)
assert.NotNil(t, p)
secret, err := p.generateClientSecret()
assert.NoError(t, err)
assert.NotEmpty(t, secret)
// Verify the JWT
token, err := jwt.Parse(secret, func(token *jwt.Token) (interface{}, error) {
return &privKey.PublicKey, nil
})
assert.NoError(t, err)
assert.True(t, token.Valid)
// Verify claims
claims, ok := token.Claims.(jwt.MapClaims)
assert.True(t, ok)
assert.Equal(t, "TEAM123456", claims["iss"])
assert.Equal(t, "com.example.client", claims["sub"])
// Verify header
assert.Equal(t, "ES256", token.Method.Alg())
assert.Equal(t, "KEY1234567", token.Header["kid"])
}
func TestAppleProviderGetLoginURL(t *testing.T) {
p, _, err := newAppleProvider()
assert.NoError(t, err)
result := p.GetLoginURL("https://example.com/callback", "state123", "nonce123", url.Values{})
assert.Contains(t, result, "response_mode=form_post")
assert.Contains(t, result, "state=state123")
assert.Contains(t, result, "redirect_uri=")
}
func TestAppleProviderRedeem(t *testing.T) {
p, _, err := newAppleProvider()
assert.NoError(t, err)
assert.NotNil(t, p)
// Create a mock ID token with claims matching the verifier configuration
expiresIn := int64(3600)
idTokenClaims := jwt.MapClaims{
"iss": appleTestIssuer,
"sub": "user123",
"aud": appleTestClientID,
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
"email": "user@example.com",
}
// Sign with RSA key (RS256) as expected by the verifier
rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048)
idToken := jwt.NewWithClaims(jwt.SigningMethodRS256, idTokenClaims)
signedIDToken, err := idToken.SignedString(rsaKey)
assert.NoError(t, err)
// Set up mock server response
body, err := json.Marshal(map[string]interface{}{
"access_token": "mock_access_token",
"token_type": "Bearer",
"expires_in": expiresIn,
"refresh_token": "mock_refresh_token",
"id_token": signedIDToken,
})
assert.NoError(t, err)
var server *httptest.Server
p.RedeemURL, server = newAppleServer(body)
defer server.Close()
session, err := p.Redeem(context.Background(), "https://example.com/callback", "code123", "")
assert.NoError(t, err)
assert.NotNil(t, session)
assert.Equal(t, "mock_access_token", session.AccessToken)
assert.Equal(t, "mock_refresh_token", session.RefreshToken)
assert.Equal(t, signedIDToken, session.IDToken)
}
func TestAppleProviderRefreshSession(t *testing.T) {
p, _, err := newAppleProvider()
assert.NoError(t, err)
assert.NotNil(t, p)
expiresIn := int64(3600)
// Set up mock server response
body, err := json.Marshal(map[string]interface{}{
"access_token": "new_access_token",
"token_type": "Bearer",
"expires_in": expiresIn,
"refresh_token": "new_refresh_token",
})
assert.NoError(t, err)
var server *httptest.Server
p.RedeemURL, server = newAppleServer(body)
defer server.Close()
session := &sessions.SessionState{
RefreshToken: "old_refresh_token",
}
refreshed, err := p.RefreshSession(context.Background(), session)
assert.NoError(t, err)
assert.True(t, refreshed)
assert.Equal(t, "new_access_token", session.AccessToken)
assert.Equal(t, "new_refresh_token", session.RefreshToken)
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
@ -21,6 +22,7 @@ type OIDCProvider struct {
*ProviderData
SkipNonce bool
AuthStyle oauth2.AuthStyle
}
const oidcDefaultScope = "openid email profile"
@ -52,6 +54,19 @@ func NewOIDCProvider(p *ProviderData, opts options.OIDCOptions) *OIDCProvider {
return &OIDCProvider{
ProviderData: p,
SkipNonce: ptr.Deref(opts.InsecureSkipNonce, options.DefaultInsecureSkipNonce),
AuthStyle: parseAuthStyle(opts.AuthStyle),
}
}
// parseAuthStyle converts the string AuthStyle option to oauth2.AuthStyle
func parseAuthStyle(style string) oauth2.AuthStyle {
switch strings.ToLower(style) {
case "inparams", "params":
return oauth2.AuthStyleInParams
case "inheader", "header":
return oauth2.AuthStyleInHeader
default:
return oauth2.AuthStyleAutoDetect
}
}
@ -88,6 +103,7 @@ func (p *OIDCProvider) Redeem(ctx context.Context, redirectURL, code, codeVerifi
ClientSecret: clientSecret,
Endpoint: oauth2.Endpoint{
TokenURL: p.RedeemURL.String(),
AuthStyle: p.AuthStyle,
},
RedirectURL: redirectURL,
}
@ -172,6 +188,7 @@ func (p *OIDCProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sessi
ClientSecret: clientSecret,
Endpoint: oauth2.Endpoint{
TokenURL: p.RedeemURL.String(),
AuthStyle: p.AuthStyle,
},
}
t := &oauth2.Token{

View File

@ -36,6 +36,10 @@ type ProviderData struct {
ClientID string
ClientSecret string
ClientSecretFile string
// ClientSecretFunc is an optional function to dynamically generate client secret.
// If set, it takes precedence over ClientSecret and ClientSecretFile.
// This is used by providers like Apple that require a JWT-signed client secret.
ClientSecretFunc func() (string, error)
Scope string
// The response mode requested from the provider or empty for default ("query")
AuthRequestResponseMode string
@ -82,6 +86,12 @@ type ProviderData struct {
func (p *ProviderData) Data() *ProviderData { return p }
func (p *ProviderData) GetClientSecret() (clientSecret string, err error) {
// If ClientSecretFunc is set, use it to generate the client secret dynamically
// This is used by providers like Apple that require a JWT-signed client secret
if p.ClientSecretFunc != nil {
return p.ClientSecretFunc()
}
if p.ClientSecret != "" || p.ClientSecretFile == "" {
return p.ClientSecret, nil
}

View File

@ -70,6 +70,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
return NewNextcloudProvider(providerData), nil
case options.OIDCProvider:
return NewOIDCProvider(providerData, providerConfig.OIDCConfig), nil
case options.AppleProvider:
return NewAppleProvider(providerData, providerConfig.AppleConfig, providerConfig.OIDCConfig)
case options.SourceHutProvider:
return NewSourceHutProvider(providerData), nil
default:
@ -194,7 +196,7 @@ func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bo
options.NextCloudProvider, options.SourceHutProvider:
return false, nil
case options.OIDCProvider, options.ADFSProvider, options.AzureProvider, options.CidaasProvider,
options.GitLabProvider, options.KeycloakOIDCProvider, options.MicrosoftEntraIDProvider:
options.GitLabProvider, options.KeycloakOIDCProvider, options.MicrosoftEntraIDProvider, options.AppleProvider:
return true, nil
default:
return false, fmt.Errorf("unknown provider type: %s", providerType)