From 613e51ccfbbee9710c4c285451f7140129b2da4b Mon Sep 17 00:00:00 2001 From: Simon Murray Date: Fri, 15 Mar 2024 13:08:13 +0000 Subject: [PATCH] Add Support of Microsoft Entra --- ...ity.unikorn-cloud.org_oauth2providers.yaml | 2 +- pkg/apis/unikorn/v1alpha1/types.go | 5 +- .../unikorn/v1alpha1/zz_generated.deepcopy.go | 7 ++- pkg/oauth2/federated.go | 22 +++++++-- pkg/oauth2/providers/factory.go | 4 +- pkg/oauth2/providers/google/provider.go | 4 +- pkg/oauth2/providers/interfaces.go | 4 +- pkg/oauth2/providers/microsoft/provider.go | 46 +++++++++++++++++++ pkg/oauth2/providers/null.go | 4 +- 9 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 pkg/oauth2/providers/microsoft/provider.go diff --git a/charts/identity/crds/identity.unikorn-cloud.org_oauth2providers.yaml b/charts/identity/crds/identity.unikorn-cloud.org_oauth2providers.yaml index f73fcd47..509820e7 100644 --- a/charts/identity/crds/identity.unikorn-cloud.org_oauth2providers.yaml +++ b/charts/identity/crds/identity.unikorn-cloud.org_oauth2providers.yaml @@ -72,10 +72,10 @@ spec: will result in undefined behaviour. enum: - google + - microsoft type: string required: - clientID - - clientSecret - displayName - issuer - type diff --git a/pkg/apis/unikorn/v1alpha1/types.go b/pkg/apis/unikorn/v1alpha1/types.go index 16c6b504..0bf73439 100644 --- a/pkg/apis/unikorn/v1alpha1/types.go +++ b/pkg/apis/unikorn/v1alpha1/types.go @@ -25,11 +25,12 @@ import ( // IdentityProviderType defines the type of identity provider, and in turn // that defines the required configuration and API interfaces. -// +kubebuilder:validation:Enum=google +// +kubebuilder:validation:Enum=google;microsoft type IdentityProviderType string const ( GoogleIdentity IdentityProviderType = "google" + MicrosoftEntra IdentityProviderType = "microsoft" ) // OAuth2ClientList is a typed list of frontend clients. @@ -107,7 +108,7 @@ type OAuth2ProviderSpec struct { // ClientID is the assigned client identifier. ClientID string `json:"clientID"` // ClientSecret is created by the IdP for token exchange. - ClientSecret string `json:"clientSecret"` + ClientSecret *string `json:"clientSecret,omitempty"` } // OAuth2ProviderStatus defines the status of the server. diff --git a/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go index 06546c94..5228ef33 100644 --- a/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go @@ -124,7 +124,7 @@ func (in *OAuth2Provider) DeepCopyInto(out *OAuth2Provider) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status return } @@ -183,6 +183,11 @@ func (in *OAuth2ProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OAuth2ProviderSpec) DeepCopyInto(out *OAuth2ProviderSpec) { *out = *in + if in.ClientSecret != nil { + in, out := &in.ClientSecret, &out.ClientSecret + *out = new(string) + **out = **in + } return } diff --git a/pkg/oauth2/federated.go b/pkg/oauth2/federated.go index 338ed014..ac904135 100644 --- a/pkg/oauth2/federated.go +++ b/pkg/oauth2/federated.go @@ -398,6 +398,15 @@ func (a *Authenticator) lookupOrganization(_ http.ResponseWriter, r *http.Reques return nil, fmt.Errorf("unsupported domain") } +// newOIDCProvider abstracts away any hacks for specific providers. +func newOIDCProvider(ctx context.Context, p *unikornv1.OAuth2Provider) (*oidc.Provider, error) { + if p.Spec.Type == unikornv1.MicrosoftEntra { + ctx = oidc.InsecureIssuerURLContext(ctx, "https://login.microsoftonline.com/{tenantid}/v2.0") + } + + return oidc.NewProvider(ctx, p.Spec.Issuer) +} + // providerAuthenticationRequest takes a client provided email address and routes it // to the correct identity provider, if we can. func (a *Authenticator) providerAuthenticationRequest(w http.ResponseWriter, r *http.Request, email string, query url.Values) { @@ -418,7 +427,7 @@ func (a *Authenticator) providerAuthenticationRequest(w http.ResponseWriter, r * driver := providers.New(providerResource.Spec.Type, organization) - provider, err := oidc.NewProvider(r.Context(), providerResource.Spec.Issuer) + provider, err := newOIDCProvider(r.Context(), &providerResource) if err != nil { log.Error(err, "failed to do OIDC discovery") return @@ -520,6 +529,8 @@ func (a *Authenticator) Login(w http.ResponseWriter, r *http.Request) { func (a *Authenticator) oidcExtractIDToken(ctx context.Context, provider *oidc.Provider, providerResource *unikornv1.OAuth2Provider, token string) (*oidc.IDToken, error) { config := &oidc.Config{ ClientID: providerResource.Spec.ClientID, + // TODO: this is a Entra-ism + SkipIssuerCheck: true, } idTokenVerifier := provider.Verifier(config) @@ -575,7 +586,7 @@ func (a *Authenticator) OIDCCallback(w http.ResponseWriter, r *http.Request) { return } - provider, err := oidc.NewProvider(r.Context(), providerResource.Spec.Issuer) + provider, err := newOIDCProvider(r.Context(), &providerResource) if err != nil { log.Error(err, "failed to do OIDC discovery") return @@ -587,10 +598,13 @@ func (a *Authenticator) OIDCCallback(w http.ResponseWriter, r *http.Request) { // the extracted code verifier. authURLParams := []oauth2.AuthCodeOption{ oauth2.SetAuthURLParam("client_id", state.ClientID), - oauth2.SetAuthURLParam("client_secret", providerResource.Spec.ClientSecret), oauth2.SetAuthURLParam("code_verifier", state.CodeVerfier), } + if providerResource.Spec.ClientSecret != nil { + authURLParams = append(authURLParams, oauth2.SetAuthURLParam("client_secret", *providerResource.Spec.ClientSecret)) + } + tokens, err := a.oidcConfig(r, &providerResource, endpoint, nil).Exchange(r.Context(), query.Get("code"), authURLParams...) if err != nil { authorizationError(w, r, state.ClientRedirectURI, ErrorServerError, "oidc code exchange failed: "+err.Error()) @@ -626,7 +640,7 @@ func (a *Authenticator) OIDCCallback(w http.ResponseWriter, r *http.Request) { driver := providers.New(providerResource.Spec.Type, &organization) - if _, err := driver.Groups(r.Context(), tokens.AccessToken); err != nil { + if _, err := driver.Groups(r.Context(), idToken, tokens.AccessToken); err != nil { authorizationError(w, r, state.ClientRedirectURI, ErrorServerError, "failed to lookup user groups: "+err.Error()) return } diff --git a/pkg/oauth2/providers/factory.go b/pkg/oauth2/providers/factory.go index 8e84b624..69d82e20 100644 --- a/pkg/oauth2/providers/factory.go +++ b/pkg/oauth2/providers/factory.go @@ -19,13 +19,15 @@ package providers import ( unikornv1 "github.com/unikorn-cloud/identity/pkg/apis/unikorn/v1alpha1" "github.com/unikorn-cloud/identity/pkg/oauth2/providers/google" + "github.com/unikorn-cloud/identity/pkg/oauth2/providers/microsoft" ) func New(providerType unikornv1.IdentityProviderType, organization *unikornv1.Organization) Provider { - //nolint:gocritic switch providerType { case unikornv1.GoogleIdentity: return google.New(organization) + case unikornv1.MicrosoftEntra: + return microsoft.New() } return newNullProvider() diff --git a/pkg/oauth2/providers/google/provider.go b/pkg/oauth2/providers/google/provider.go index 25946bee..9b5bfc67 100644 --- a/pkg/oauth2/providers/google/provider.go +++ b/pkg/oauth2/providers/google/provider.go @@ -25,6 +25,8 @@ import ( "net/http" "net/url" + "github.com/coreos/go-oidc/v3/oidc" + unikornv1 "github.com/unikorn-cloud/identity/pkg/apis/unikorn/v1alpha1" ) @@ -57,7 +59,7 @@ type Groups struct { Groups []Group `json:"groups"` } -func (p *Provider) Groups(ctx context.Context, accessToken string) ([]string, error) { +func (p *Provider) Groups(ctx context.Context, idToken *oidc.IDToken, accessToken string) ([]string, error) { if p.organization.Spec.ProviderOptions == nil || p.organization.Spec.ProviderOptions.Google == nil { return nil, nil } diff --git a/pkg/oauth2/providers/interfaces.go b/pkg/oauth2/providers/interfaces.go index 2e137735..b938136c 100644 --- a/pkg/oauth2/providers/interfaces.go +++ b/pkg/oauth2/providers/interfaces.go @@ -18,6 +18,8 @@ package providers import ( "context" + + "github.com/coreos/go-oidc/v3/oidc" ) type Provider interface { @@ -26,5 +28,5 @@ type Provider interface { Scopes() []string // Groups returns a list of groups the user belongs to. - Groups(ctx context.Context, accessToken string) ([]string, error) + Groups(ctx context.Context, idToken *oidc.IDToken, accessToken string) ([]string, error) } diff --git a/pkg/oauth2/providers/microsoft/provider.go b/pkg/oauth2/providers/microsoft/provider.go new file mode 100644 index 00000000..49bcd432 --- /dev/null +++ b/pkg/oauth2/providers/microsoft/provider.go @@ -0,0 +1,46 @@ +/* +Copyright 2024 the Unikorn Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package microsoft + +import ( + "context" + + "github.com/coreos/go-oidc/v3/oidc" +) + +type Provider struct { +} + +func New() *Provider { + return &Provider{} +} + +func (*Provider) Scopes() []string { + return []string{} +} + +func (p *Provider) Groups(ctx context.Context, idToken *oidc.IDToken, accessToken string) ([]string, error) { + var claims struct { + Groups []string `json:"groups"` + } + + if err := idToken.Claims(&claims); err != nil { + return nil, err + } + + return claims.Groups, nil +} diff --git a/pkg/oauth2/providers/null.go b/pkg/oauth2/providers/null.go index cf596420..7079b017 100644 --- a/pkg/oauth2/providers/null.go +++ b/pkg/oauth2/providers/null.go @@ -18,6 +18,8 @@ package providers import ( "context" + + "github.com/coreos/go-oidc/v3/oidc" ) // nullProvider does nothing. @@ -31,6 +33,6 @@ func (*nullProvider) Scopes() []string { return nil } -func (*nullProvider) Groups(ctx context.Context, accessToken string) ([]string, error) { +func (*nullProvider) Groups(ctx context.Context, idToken *oidc.IDToken, accessToken string) ([]string, error) { return nil, nil }