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)
}