Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v17] Expose /.well-known/jwks-okta for Okta API services type App #50177

Merged
merged 1 commit into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,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))

Expand Down
65 changes: 65 additions & 0 deletions lib/web/jwt.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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
}
41 changes: 0 additions & 41 deletions lib/web/oidcidp.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@
package web

import (
"context"
"net/http"

"github.com/gravitational/trace"
"github.com/julienschmidt/httprouter"

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

Expand All @@ -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
Expand Down
33 changes: 3 additions & 30 deletions lib/web/oidcidp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
32 changes: 32 additions & 0 deletions lib/web/okta.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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 */)
}
46 changes: 46 additions & 0 deletions lib/web/okta_test.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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)
}
22 changes: 1 addition & 21 deletions lib/web/spiffe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading