* feat(op): dynamic issuer depending on request / host BREAKING CHANGE: The OpenID Provider package is now able to handle multiple issuers with a single storage implementation. The issuer will be selected from the host of the request and passed into the context, where every function can read it from if necessary. This results in some fundamental changes: - `Configuration` interface: - `Issuer() string` has been changed to `IssuerFromRequest(r *http.Request) string` - `Insecure() bool` has been added - OpenIDProvider interface and dependants: - `Issuer` has been removed from Config struct - `NewOpenIDProvider` now takes an additional parameter `issuer` and returns a pointer to the public/default implementation and not an OpenIDProvider interface: `NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opOpts ...Option) (OpenIDProvider, error)` changed to `NewOpenIDProvider(ctx context.Context, issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error)` - therefore the parameter type Option changed to the public type as well: `Option func(o *Provider) error` - `AuthCallbackURL(o OpenIDProvider) func(string) string` has been changed to `AuthCallbackURL(o OpenIDProvider) func(context.Context, string) string` - `IDTokenHintVerifier() IDTokenHintVerifier` (Authorizer, OpenIDProvider, SessionEnder interfaces), `AccessTokenVerifier() AccessTokenVerifier` (Introspector, OpenIDProvider, Revoker, UserinfoProvider interfaces) and `JWTProfileVerifier() JWTProfileVerifier` (IntrospectorJWTProfile, JWTAuthorizationGrantExchanger, OpenIDProvider, RevokerJWTProfile interfaces) now take a context.Context parameter `IDTokenHintVerifier(context.Context) IDTokenHintVerifier`, `AccessTokenVerifier(context.Context) AccessTokenVerifier` and `JWTProfileVerifier(context.Context) JWTProfileVerifier` - `OidcDevMode` (CAOS_OIDC_DEV) environment variable check has been removed, use `WithAllowInsecure()` Option - Signing: the signer is not kept in memory anymore, but created on request from the loaded key: - `Signer` interface and func `NewSigner` have been removed - `ReadySigner(s Signer) ProbesFn` has been removed - `CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration` has been changed to `CreateDiscoveryConfig(r *http.Request, config Configuration, storage DiscoverStorage) *oidc.DiscoveryConfiguration` - `Storage` interface: - `GetSigningKey(context.Context, chan<- jose.SigningKey)` has been changed to `SigningKey(context.Context) (SigningKey, error)` - `KeySet(context.Context) ([]Key, error)` has been added - `GetKeySet(context.Context) (*jose.JSONWebKeySet, error)` has been changed to `KeySet(context.Context) ([]Key, error)` - `SigAlgorithms(s Signer) []string` has been changed to `SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string` - KeyProvider interface: `GetKeySet(context.Context) (*jose.JSONWebKeySet, error)` has been changed to `KeySet(context.Context) ([]Key, error)` - `CreateIDToken`: the Signer parameter has been removed * move example * fix examples * fix mocks * update readme * fix examples and update usage * update go module version to v2 * build branch * fix(module): rename caos to zitadel * fix: add state in access token response (implicit flow) * fix: encode auth response correctly (when using query in redirect uri) * fix query param handling * feat: add all optional claims of the introspection response * fix: use default redirect uri when not passed * fix: exchange cors library and add `X-Requested-With` to Access-Control-Request-Headers (#261) * feat(op): add support for client credentials * fix mocks and test * feat: allow to specify token type of JWT Profile Grant * document JWTProfileTokenStorage * cleanup * rp: fix integration test test username needed to be suffixed by issuer domain * chore(deps): bump golang.org/x/text from 0.5.0 to 0.6.0 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.5.0 to 0.6.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.5.0...v0.6.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * op: mock: cleanup commented code * op: remove duplicate code code duplication caused by merge conflict selections --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Livio Amstutz <livio.a@gmail.com> Co-authored-by: adlerhurst <silvan.reusser@gmail.com> Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
138 lines
4.5 KiB
Go
138 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
|
|
"github.com/gorilla/mux"
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/zitadel/oidc/v2/example/server/storage"
|
|
"github.com/zitadel/oidc/v2/pkg/op"
|
|
)
|
|
|
|
const (
|
|
pathLoggedOut = "/logged-out"
|
|
)
|
|
|
|
var (
|
|
hostnames = []string{
|
|
"localhost", //note that calling 127.0.0.1 / ::1 won't work as the hostname does not match
|
|
"oidc.local", //add this to your hosts file (pointing to 127.0.0.1)
|
|
//feel free to add more...
|
|
}
|
|
)
|
|
|
|
func init() {
|
|
storage.RegisterClients(
|
|
storage.NativeClient("native"),
|
|
storage.WebClient("web", "secret"),
|
|
storage.WebClient("api", "secret"),
|
|
)
|
|
}
|
|
|
|
func main() {
|
|
ctx := context.Background()
|
|
|
|
port := "9998"
|
|
issuers := make([]string, len(hostnames))
|
|
for i, hostname := range hostnames {
|
|
issuers[i] = fmt.Sprintf("http://%s:%s/", hostname, port)
|
|
}
|
|
|
|
//the OpenID Provider requires a 32-byte key for (token) encryption
|
|
//be sure to create a proper crypto random key and manage it securely!
|
|
key := sha256.Sum256([]byte("test"))
|
|
|
|
router := mux.NewRouter()
|
|
|
|
//for simplicity, we provide a very small default page for users who have signed out
|
|
router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
|
|
_, err := w.Write([]byte("signed out successfully"))
|
|
if err != nil {
|
|
log.Printf("error serving logged out page: %v", err)
|
|
}
|
|
})
|
|
|
|
//the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
|
|
//this might be the layer for accessing your database
|
|
//in this example it will be handled in-memory
|
|
//the NewMultiStorage is able to handle multiple issuers
|
|
storage := storage.NewMultiStorage(issuers)
|
|
|
|
//creation of the OpenIDProvider with the just created in-memory Storage
|
|
provider, err := newDynamicOP(ctx, storage, key)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
//the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process
|
|
//for the simplicity of the example this means a simple page with username and password field
|
|
//be sure to provide an IssuerInterceptor with the IssuerFromRequest from the OP so the login can select / and pass it to the storage
|
|
l := NewLogin(storage, op.AuthCallbackURL(provider), op.NewIssuerInterceptor(provider.IssuerFromRequest))
|
|
|
|
//regardless of how many pages / steps there are in the process, the UI must be registered in the router,
|
|
//so we will direct all calls to /login to the login UI
|
|
router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router))
|
|
|
|
//we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
|
|
//is served on the correct path
|
|
//
|
|
//if your issuer ends with a path (e.g. http://localhost:9998/custom/path/),
|
|
//then you would have to set the path prefix (/custom/path/):
|
|
//router.PathPrefix("/custom/path/").Handler(http.StripPrefix("/custom/path", provider.HttpHandler()))
|
|
router.PathPrefix("/").Handler(provider.HttpHandler())
|
|
|
|
server := &http.Server{
|
|
Addr: ":" + port,
|
|
Handler: router,
|
|
}
|
|
err = server.ListenAndServe()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
<-ctx.Done()
|
|
}
|
|
|
|
// newDynamicOP will create an OpenID Provider for localhost on a specified port with a given encryption key
|
|
// and a predefined default logout uri
|
|
// it will enable all options (see descriptions)
|
|
func newDynamicOP(ctx context.Context, storage op.Storage, key [32]byte) (*op.Provider, error) {
|
|
config := &op.Config{
|
|
CryptoKey: key,
|
|
|
|
//will be used if the end_session endpoint is called without a post_logout_redirect_uri
|
|
DefaultLogoutRedirectURI: pathLoggedOut,
|
|
|
|
//enables code_challenge_method S256 for PKCE (and therefore PKCE in general)
|
|
CodeMethodS256: true,
|
|
|
|
//enables additional client_id/client_secret authentication by form post (not only HTTP Basic Auth)
|
|
AuthMethodPost: true,
|
|
|
|
//enables additional authentication by using private_key_jwt
|
|
AuthMethodPrivateKeyJWT: true,
|
|
|
|
//enables refresh_token grant use
|
|
GrantTypeRefreshToken: true,
|
|
|
|
//enables use of the `request` Object parameter
|
|
RequestObjectSupported: true,
|
|
|
|
//this example has only static texts (in English), so we'll set the here accordingly
|
|
SupportedUILocales: []language.Tag{language.English},
|
|
}
|
|
handler, err := op.NewDynamicOpenIDProvider(ctx, "/", config, storage,
|
|
//we must explicitly allow the use of the http issuer
|
|
op.WithAllowInsecure(),
|
|
//as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
|
|
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return handler, nil
|
|
}
|