dexidp / dex

OpenID Connect (OIDC) identity and OAuth 2.0 provider with pluggable connectors
https://dexidp.io
Apache License 2.0
9.37k stars 1.69k forks source link

Support Upstream OIDC Providers without a ClientSecret #3194

Open cameronbrunner opened 10 months ago

cameronbrunner commented 10 months ago

Preflight Checklist

Problem Description

When using the OIDC connector Dex should support a secure method for distributing a common configuration file that could be used by multiple instances of a given application. Presently this is not possible as the OIDC connector requires both a ClientID and ClientSecret leading to either the distribution of the ClientSecret to all application instances or a unique ClientID for application instances. The former is insecure and the later is unfeasible with significantly large numbers of application instances.

Proposed Solution

OIDC classifies client applications into two categories, Confidential and Public. Dex presently behaves only as a Confidential application and thus requires a ClientID and ClientSecret. I propose that support for Public be added as an option to the OIDC connector.

I believe all that is required is: 1) Add a new configuration option to enable Public mode for connectors. 2) When public mode is enabled allow for an empty ClientSecret 3) When public mode is enabled add a S256 Challenge option when generating the OIDC AuthCodeURL. 4) Use the appropriate verifier when running 'Exchange' later on in the flow.

The majority fo the actual work is handled in the downstream golang oidc code and was added in April of this year:

https://github.com/golang/oauth2/issues/603 https://github.com/golang/go/issues/59835

Some sample code.

Add something like this to the LoginURL function (https://github.com/dexidp/dex/blob/e41a28bf27225ab503eb9feef4feedd03bb4ac71/connector/oidc/oidc.go#L253)

    if c.Public {
        verifier := oauth2.GenerateVerifier()
        opts = append(opts, oauth2.S256ChallengeOption(verifier))
        // Add state and verifier to a connector level cache with a short expiration.  Referenced in Exchange later.
       }

And this to HandleCallback (https://github.com/dexidp/dex/blob/e41a28bf27225ab503eb9feef4feedd03bb4ac71/connector/oidc/oidc.go#L291C18-L291C18)

    state := q.Get("state")
    // Look up state in cache
    var tok *oauth2.Token
    var err error
    if cachedVerifier == nil {
        tok, err = c.oauth2Config.Exchange(ctx, code)
    } else {
        tok, err = c.oauth2Config.Exchange(ctx, code, oauth2.VerifierOption(cachedVerifier))
    }

Alternatives Considered

1) Distributed a shared secret to multiple applications which would be too insecure. 2) Creating secrets for each application instance which could be infeasible with large numbers of dynamic applications.

Additional Information

Discussion on Confidential and Public OIDC applications - https://auth0.com/docs/get-started/applications/confidential-and-public-applications

cameronbrunner commented 10 months ago

FWIW - I have a separate golang application where I have added a variant of the suggested improvement and have verified its use with Okta as an upstream OIDC provider.

cameronbrunner commented 9 months ago

Another implementation offered here #3188