Merge 15dfd6e617 into 1f049e5ebd
This commit is contained in:
commit
a7c1d8a706
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
## Changes since v7.15.3
|
## 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
|
# V7.15.3
|
||||||
|
|
||||||
## Release Highlights
|
## Release Highlights
|
||||||
|
|
|
||||||
|
|
@ -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. |
|
| `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) |
|
| `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
|
### AzureOptions
|
||||||
|
|
||||||
(**Appears on:** [Provider](#provider))
|
(**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. |
|
| `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. |
|
| `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. |
|
| `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
|
### Provider
|
||||||
|
|
||||||
|
|
@ -574,6 +588,7 @@ Provider holds all configuration for a single provider
|
||||||
| `googleConfig` | _[GoogleOptions](#googleoptions)_ | GoogleConfig holds all configurations for Google 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. |
|
| `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. |
|
| `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. |
|
| `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 |
|
| `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. |
|
| `name` | _string_ | Name is the providers display name<br/>if set, it will be shown to the users in the login page. |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
```
|
||||||
|
|
@ -9,6 +9,7 @@ with Redirect URI(s) for the domain you intend to run `oauth2-proxy` on.
|
||||||
Valid providers are :
|
Valid providers are :
|
||||||
|
|
||||||
- [ADFS](adfs.md)
|
- [ADFS](adfs.md)
|
||||||
|
- [Apple](apple.md)
|
||||||
- [Bitbucket](bitbucket.md)
|
- [Bitbucket](bitbucket.md)
|
||||||
- [Cidaas](cidaas.md)
|
- [Cidaas](cidaas.md)
|
||||||
- [CiscoDuo](cisco_duo.md)
|
- [CiscoDuo](cisco_duo.md)
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,8 @@ type Provider struct {
|
||||||
OIDCConfig OIDCOptions `yaml:"oidcConfig,omitempty"`
|
OIDCConfig OIDCOptions `yaml:"oidcConfig,omitempty"`
|
||||||
// LoginGovConfig holds all configurations for LoginGov provider.
|
// LoginGovConfig holds all configurations for LoginGov provider.
|
||||||
LoginGovConfig LoginGovOptions `yaml:"loginGovConfig,omitempty"`
|
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.
|
// ID should be a unique identifier for the provider.
|
||||||
// This value is required for all providers.
|
// This value is required for all providers.
|
||||||
|
|
@ -198,6 +200,9 @@ const (
|
||||||
|
|
||||||
// SourceHutProvider is the provider type for SourceHut
|
// SourceHutProvider is the provider type for SourceHut
|
||||||
SourceHutProvider ProviderType = "sourcehut"
|
SourceHutProvider ProviderType = "sourcehut"
|
||||||
|
|
||||||
|
// AppleProvider is the provider type for Apple Sign in with Apple
|
||||||
|
AppleProvider ProviderType = "apple"
|
||||||
)
|
)
|
||||||
|
|
||||||
type KeycloakOptions struct {
|
type KeycloakOptions struct {
|
||||||
|
|
@ -326,6 +331,13 @@ type OIDCOptions struct {
|
||||||
// between this list and the provider's discovered supported algorithms.
|
// between this list and the provider's discovered supported algorithms.
|
||||||
// By default `RS256` is used if nothing has been discovered or specified.
|
// By default `RS256` is used if nothing has been discovered or specified.
|
||||||
EnabledSigningAlgs []string `yaml:"enabledSigningAlgs,omitempty"`
|
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 {
|
type LoginGovOptions struct {
|
||||||
|
|
@ -337,6 +349,17 @@ type LoginGovOptions struct {
|
||||||
PubJWKURL string `yaml:"pubjwkURL,omitempty"`
|
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
|
// Legacy default providers configuration
|
||||||
func providerDefaults() Providers {
|
func providerDefaults() Providers {
|
||||||
providers := Providers{
|
providers := Providers{
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,11 @@ func providerRequiresClientSecret(provider options.Provider) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apple uses a private key to dynamically generate client_secret JWTs
|
||||||
|
if provider.Type == options.AppleProvider {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
|
@ -21,6 +22,7 @@ type OIDCProvider struct {
|
||||||
*ProviderData
|
*ProviderData
|
||||||
|
|
||||||
SkipNonce bool
|
SkipNonce bool
|
||||||
|
AuthStyle oauth2.AuthStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
const oidcDefaultScope = "openid email profile"
|
const oidcDefaultScope = "openid email profile"
|
||||||
|
|
@ -52,6 +54,19 @@ func NewOIDCProvider(p *ProviderData, opts options.OIDCOptions) *OIDCProvider {
|
||||||
return &OIDCProvider{
|
return &OIDCProvider{
|
||||||
ProviderData: p,
|
ProviderData: p,
|
||||||
SkipNonce: ptr.Deref(opts.InsecureSkipNonce, options.DefaultInsecureSkipNonce),
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +102,8 @@ func (p *OIDCProvider) Redeem(ctx context.Context, redirectURL, code, codeVerifi
|
||||||
ClientID: p.ClientID,
|
ClientID: p.ClientID,
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: clientSecret,
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
TokenURL: p.RedeemURL.String(),
|
TokenURL: p.RedeemURL.String(),
|
||||||
|
AuthStyle: p.AuthStyle,
|
||||||
},
|
},
|
||||||
RedirectURL: redirectURL,
|
RedirectURL: redirectURL,
|
||||||
}
|
}
|
||||||
|
|
@ -171,7 +187,8 @@ func (p *OIDCProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sessi
|
||||||
ClientID: p.ClientID,
|
ClientID: p.ClientID,
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: clientSecret,
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
TokenURL: p.RedeemURL.String(),
|
TokenURL: p.RedeemURL.String(),
|
||||||
|
AuthStyle: p.AuthStyle,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
t := &oauth2.Token{
|
t := &oauth2.Token{
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,11 @@ type ProviderData struct {
|
||||||
ClientID string
|
ClientID string
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
ClientSecretFile string
|
ClientSecretFile string
|
||||||
Scope 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")
|
// The response mode requested from the provider or empty for default ("query")
|
||||||
AuthRequestResponseMode string
|
AuthRequestResponseMode string
|
||||||
// The picked CodeChallenge Method or empty if none.
|
// The picked CodeChallenge Method or empty if none.
|
||||||
|
|
@ -82,6 +86,12 @@ type ProviderData struct {
|
||||||
func (p *ProviderData) Data() *ProviderData { return p }
|
func (p *ProviderData) Data() *ProviderData { return p }
|
||||||
|
|
||||||
func (p *ProviderData) GetClientSecret() (clientSecret string, err error) {
|
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 == "" {
|
if p.ClientSecret != "" || p.ClientSecretFile == "" {
|
||||||
return p.ClientSecret, nil
|
return p.ClientSecret, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
|
||||||
return NewNextcloudProvider(providerData), nil
|
return NewNextcloudProvider(providerData), nil
|
||||||
case options.OIDCProvider:
|
case options.OIDCProvider:
|
||||||
return NewOIDCProvider(providerData, providerConfig.OIDCConfig), nil
|
return NewOIDCProvider(providerData, providerConfig.OIDCConfig), nil
|
||||||
|
case options.AppleProvider:
|
||||||
|
return NewAppleProvider(providerData, providerConfig.AppleConfig, providerConfig.OIDCConfig)
|
||||||
case options.SourceHutProvider:
|
case options.SourceHutProvider:
|
||||||
return NewSourceHutProvider(providerData), nil
|
return NewSourceHutProvider(providerData), nil
|
||||||
default:
|
default:
|
||||||
|
|
@ -194,7 +196,7 @@ func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bo
|
||||||
options.NextCloudProvider, options.SourceHutProvider:
|
options.NextCloudProvider, options.SourceHutProvider:
|
||||||
return false, nil
|
return false, nil
|
||||||
case options.OIDCProvider, options.ADFSProvider, options.AzureProvider, options.CidaasProvider,
|
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
|
return true, nil
|
||||||
default:
|
default:
|
||||||
return false, fmt.Errorf("unknown provider type: %s", providerType)
|
return false, fmt.Errorf("unknown provider type: %s", providerType)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue