diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index f1246187a5afe..796e8351ca6f7 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -1009,6 +1009,9 @@ func (h *Handler) bindDefaultEndpoints() { // SAML IDP integration endpoints h.GET("/webapi/scripts/integrations/configure/gcp-workforce-saml.sh", h.WithLimiter(h.gcpWorkforceConfigScript)) + // Okta integration endpoints. + h.GET("/.well-known/jwks-okta", h.WithLimiter(h.jwksOkta)) + // Azure OIDC integration endpoints h.GET("/webapi/scripts/integrations/configure/azureoidc.sh", h.WithLimiter(h.azureOIDCConfigure)) diff --git a/lib/web/jwt.go b/lib/web/jwt.go new file mode 100644 index 0000000000000..40ae3011db979 --- /dev/null +++ b/lib/web/jwt.go @@ -0,0 +1,65 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package web + +import ( + "context" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/jwt" +) + +func (h *Handler) jwks(ctx context.Context, caType types.CertAuthType, includeBlankKeyID bool) (*JWKSResponse, error) { + clusterName, err := h.GetProxyClient().GetDomainName(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + // Fetch the JWT public keys only. + ca, err := h.GetProxyClient().GetCertAuthority(ctx, types.CertAuthID{ + Type: caType, + DomainName: clusterName, + }, false /* loadKeys */) + if err != nil { + return nil, trace.Wrap(err) + } + + pairs := ca.GetTrustedJWTKeyPairs() + + // Create response and allocate space for the keys. + var resp JWKSResponse + resp.Keys = make([]jwt.JWK, 0, len(pairs)) + + // Loop over and all add public keys in JWK format. + for _, key := range pairs { + jwk, err := jwt.MarshalJWK(key.PublicKey) + if err != nil { + return nil, trace.Wrap(err) + } + resp.Keys = append(resp.Keys, jwk) + + // Return an additional copy of the same JWK + // with KeyID set to the empty string for compatibility. + if includeBlankKeyID { + jwk.KeyID = "" + resp.Keys = append(resp.Keys, jwk) + } + } + return &resp, nil +} diff --git a/lib/web/oidcidp.go b/lib/web/oidcidp.go index 28a9b7b7465b0..7b9c433f378f7 100644 --- a/lib/web/oidcidp.go +++ b/lib/web/oidcidp.go @@ -19,7 +19,6 @@ package web import ( - "context" "net/http" "github.com/gravitational/trace" @@ -27,7 +26,6 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/integrations/awsoidc" - "github.com/gravitational/teleport/lib/jwt" "github.com/gravitational/teleport/lib/utils/oidc" ) @@ -51,45 +49,6 @@ func (h *Handler) jwksOIDC(_ http.ResponseWriter, r *http.Request, _ httprouter. return h.jwks(r.Context(), types.OIDCIdPCA, true) } -func (h *Handler) jwks(ctx context.Context, caType types.CertAuthType, includeBlankKeyID bool) (*JWKSResponse, error) { - clusterName, err := h.GetProxyClient().GetDomainName(ctx) - if err != nil { - return nil, trace.Wrap(err) - } - - // Fetch the JWT public keys only. - ca, err := h.GetProxyClient().GetCertAuthority(ctx, types.CertAuthID{ - Type: caType, - DomainName: clusterName, - }, false /* loadKeys */) - if err != nil { - return nil, trace.Wrap(err) - } - - pairs := ca.GetTrustedJWTKeyPairs() - - // Create response and allocate space for the keys. - var resp JWKSResponse - resp.Keys = make([]jwt.JWK, 0, len(pairs)) - - // Loop over and all add public keys in JWK format. - for _, key := range pairs { - jwk, err := jwt.MarshalJWK(key.PublicKey) - if err != nil { - return nil, trace.Wrap(err) - } - resp.Keys = append(resp.Keys, jwk) - - // Return an additional copy of the same JWK - // with KeyID set to the empty string for compatibility. - if includeBlankKeyID { - jwk.KeyID = "" - resp.Keys = append(resp.Keys, jwk) - } - } - return &resp, nil -} - // thumbprint returns the thumbprint as required by AWS when adding an OIDC Identity Provider. // This is documented here: // https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html diff --git a/lib/web/oidcidp_test.go b/lib/web/oidcidp_test.go index a663a13db6a92..20c9063a7fcb0 100644 --- a/lib/web/oidcidp_test.go +++ b/lib/web/oidcidp_test.go @@ -72,41 +72,14 @@ func TestOIDCIdPPublicEndpoints(t *testing.T) { resp, err = publicClt.Get(ctx, gotConfiguration.JWKSURI, nil) require.NoError(t, err) - type jwksKey struct { - Use string `json:"use"` - KeyID *string `json:"kid"` - KeyType string `json:"kty"` - Alg string `json:"alg"` - } - type jwksKeys struct { - Keys []jwksKey `json:"keys"` - } - - var gotKeys jwksKeys + var gotKeys JWKSResponse err = json.Unmarshal(resp.Bytes(), &gotKeys) require.NoError(t, err) // Expect the same key twice, once with a synthesized Key ID, and once with an empty Key ID for compatibility. require.Len(t, gotKeys.Keys, 2) - require.NotEmpty(t, *gotKeys.Keys[0].KeyID) - require.Equal(t, "", *gotKeys.Keys[1].KeyID) - expectedKeys := jwksKeys{ - Keys: []jwksKey{ - { - Use: "sig", - KeyType: "RSA", - Alg: "RS256", - KeyID: gotKeys.Keys[0].KeyID, - }, - { - Use: "sig", - KeyType: "RSA", - Alg: "RS256", - KeyID: new(string), - }, - }, - } - require.Equal(t, expectedKeys, gotKeys) + require.NotEmpty(t, gotKeys.Keys[0].KeyID) + require.Empty(t, gotKeys.Keys[1].KeyID) } func TestThumbprint(t *testing.T) { diff --git a/lib/web/okta.go b/lib/web/okta.go new file mode 100644 index 0000000000000..67dec290784ae --- /dev/null +++ b/lib/web/okta.go @@ -0,0 +1,32 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package web + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" + + "github.com/gravitational/teleport/api/types" +) + +// jwksOkta returns public keys used to verify JWT tokens signed for use with Okta API Service App +// machine-to-machine authentication. +// https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/main/ +func (h *Handler) jwksOkta(_ http.ResponseWriter, r *http.Request, _ httprouter.Params) (interface{}, error) { + return h.jwks(r.Context(), types.OktaCA, false /* includeBlankKeyID */) +} diff --git a/lib/web/okta_test.go b/lib/web/okta_test.go new file mode 100644 index 0000000000000..b884aa5f85283 --- /dev/null +++ b/lib/web/okta_test.go @@ -0,0 +1,46 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package web + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestJWKSOktaPublicEndpoint ensures the public endpoint for the Okta API Service App integration +// is available. +func TestJWKSOktaPublicEndpoint(t *testing.T) { + t.Parallel() + ctx := context.Background() + env := newWebPack(t, 1) + proxy := env.proxies[0] + + publicClt := proxy.newClient(t) + + resp, err := publicClt.Get(ctx, publicClt.Endpoint(".well-known/jwks-okta"), nil) + require.NoError(t, err) + + var gotKeys JWKSResponse + err = json.Unmarshal(resp.Bytes(), &gotKeys) + require.NoError(t, err) + + require.Len(t, gotKeys.Keys, 1) + require.NotEmpty(t, gotKeys.Keys[0].KeyID) +} diff --git a/lib/web/spiffe_test.go b/lib/web/spiffe_test.go index eef680d411123..1eb49b33b4369 100644 --- a/lib/web/spiffe_test.go +++ b/lib/web/spiffe_test.go @@ -131,30 +131,10 @@ func TestSPIFFEJWTPublicEndpoints(t *testing.T) { resp, err = publicClt.Get(ctx, gotConfiguration.JWKSURI, nil) require.NoError(t, err) - type jwksKey struct { - Use string `json:"use"` - KeyID string `json:"kid"` - KeyType string `json:"kty"` - Alg string `json:"alg"` - } - type jwksKeys struct { - Keys []jwksKey `json:"keys"` - } - gotKeys := jwksKeys{} + var gotKeys JWKSResponse err = json.Unmarshal(resp.Bytes(), &gotKeys) require.NoError(t, err) require.Len(t, gotKeys.Keys, 1) require.NotEmpty(t, gotKeys.Keys[0].KeyID) - expectedKeys := jwksKeys{ - Keys: []jwksKey{ - { - Use: "sig", - KeyType: "EC", - Alg: "ES256", - KeyID: gotKeys.Keys[0].KeyID, - }, - }, - } - require.Equal(t, expectedKeys, gotKeys) }