Skip to content

Commit

Permalink
Expose /.well-known/jwks-okta for Okta API services type App (#50177)
Browse files Browse the repository at this point in the history
  • Loading branch information
kopiczko authored Dec 13, 2024
1 parent ec2a925 commit f0549aa
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 92 deletions.
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)
}

0 comments on commit f0549aa

Please sign in to comment.