Skip to content

Commit 5cc9e06

Browse files
committed
refactor(authn): Simplify OIDC config to use issuer and audience, enhance JWKS handling
1 parent d79624c commit 5cc9e06

File tree

13 files changed

+610
-507
lines changed

13 files changed

+610
-507
lines changed

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ require (
6565
require (
6666
dario.cat/mergo v1.0.0 // indirect
6767
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
68+
github.com/MicahParks/keyfunc v1.9.0 // indirect
6869
github.com/Microsoft/go-winio v0.6.1 // indirect
6970
github.com/Microsoft/hcsshim v0.11.4 // indirect
7071
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
@@ -90,7 +91,9 @@ require (
9091
github.com/gorilla/schema v1.2.0 // indirect
9192
github.com/gorilla/securecookie v1.1.1 // indirect
9293
github.com/hashicorp/errwrap v1.1.0 // indirect
94+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
9395
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
96+
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
9497
github.com/hashicorp/golang-lru v0.5.4 // indirect
9598
github.com/hashicorp/hcl v1.0.0 // indirect
9699
github.com/inconshreveable/mousetrap v1.1.0 // indirect

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
5252
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
5353
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
5454
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
55+
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
56+
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
5557
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
5658
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
5759
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
@@ -142,6 +144,7 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4
142144
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
143145
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
144146
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
147+
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
145148
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
146149
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
147150
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -232,13 +235,18 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3Kp
232235
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
233236
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
234237
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
238+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
239+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
240+
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
235241
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
236242
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
237243
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
238244
github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c=
239245
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
240246
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
241247
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
248+
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
249+
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
242250
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
243251
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
244252
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=

internal/authn/oidc/authn.go

Lines changed: 211 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ package oidc
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"fmt"
8+
"io"
79
"net/http"
10+
"strings"
811
"time"
912

10-
"google.golang.org/grpc/codes"
11-
"google.golang.org/grpc/status"
12-
13-
grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
14-
"github.com/zitadel/oidc/pkg/client"
15-
"github.com/zitadel/oidc/pkg/client/rp"
16-
"github.com/zitadel/oidc/pkg/oidc"
13+
"github.com/MicahParks/keyfunc"
14+
"github.com/golang-jwt/jwt/v4"
15+
grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
16+
"github.com/hashicorp/go-retryablehttp"
1717

1818
"github.com/Permify/permify/internal/config"
1919
base "github.com/Permify/permify/pkg/pb/base/v1"
@@ -24,58 +24,233 @@ type Authenticator interface {
2424
Authenticate(ctx context.Context) error
2525
}
2626

27-
// Authn - Oidc verifier structure
27+
// Authn holds configuration for OIDC authentication, including issuer, audience, and key details.
2828
type Authn struct {
29-
verifier rp.IDTokenVerifier
29+
// IssuerURL is the URL of the OIDC issuer.
30+
IssuerURL string
31+
32+
// Audience is the intended audience of the tokens, typically the client ID.
33+
Audience string
34+
35+
// JwksURI is the URL to fetch the JSON Web Key Set (JWKS) from.
36+
JwksURI string
37+
38+
// JWKs holds the JWKS fetched from JwksURI for validating tokens.
39+
JWKs *keyfunc.JWKS
40+
41+
// httpClient is used to make HTTP requests, e.g., to fetch the JWKS.
42+
httpClient *http.Client
3043
}
3144

32-
// NewOidcAuthn - Create new Oidc verifier
33-
func NewOidcAuthn(_ context.Context, cfg config.Oidc) (*Authn, error) {
34-
dis, err := client.Discover(cfg.Issuer, http.DefaultClient)
45+
// NewOidcAuthn creates a new instance of Authn configured for OIDC authentication.
46+
// It initializes the HTTP client with retry capabilities, sets up the OIDC issuer and audience,
47+
// and attempts to fetch the JWKS keys from the issuer's JWKsURI.
48+
func NewOidcAuthn(_ context.Context, audience config.Oidc) (*Authn, error) {
49+
// Initialize a new retryable HTTP client to handle transient network errors
50+
// by retrying failed HTTP requests. The logger is disabled for cleaner output.
51+
client := retryablehttp.NewClient()
52+
client.Logger = nil // Disabling logging for the HTTP client
53+
54+
// Create a new instance of Authn with the provided issuer URL and audience.
55+
// The httpClient is set to the standard net/http client wrapped with retry logic.
56+
oidc := &Authn{
57+
IssuerURL: audience.Issuer,
58+
Audience: audience.Audience,
59+
httpClient: client.StandardClient(), // Wrap retryable client as a standard http.Client
60+
}
61+
62+
// Attempt to fetch the JWKS keys from the OIDC provider.
63+
// This is crucial for setting up OIDC authentication as it enables token validation.
64+
err := oidc.fetchKeys()
3565
if err != nil {
66+
// If fetching keys fails, return an error to prevent initialization of a non-functional Authn instance.
3667
return nil, err
3768
}
38-
remoteKeySet := rp.NewRemoteKeySet(http.DefaultClient, dis.JwksURI)
39-
verifier := rp.NewIDTokenVerifier(dis.Issuer, cfg.ClientID, remoteKeySet,
40-
rp.WithSupportedSigningAlgorithms(dis.IDTokenSigningAlgValuesSupported...))
4169

42-
return &Authn{verifier: verifier}, nil
70+
// Return the initialized Authn instance, ready for use in OIDC authentication.
71+
return oidc, nil
4372
}
4473

45-
// Authenticate - Checking whether JWT token is signed by the provider and is valid
46-
func (t *Authn) Authenticate(ctx context.Context) error {
47-
rawToken, err := grpcAuth.AuthFromMD(ctx, "Bearer")
74+
// Authenticate validates the authentication token from the request context.
75+
func (oidc *Authn) Authenticate(requestContext context.Context) error {
76+
// Extract the authentication header from the metadata in the request context.
77+
authHeader, err := grpcauth.AuthFromMD(requestContext, "Bearer")
4878
if err != nil {
79+
// Return an error if the bearer token is missing from the authentication header.
4980
return errors.New(base.ErrorCode_ERROR_CODE_MISSING_BEARER_TOKEN.String())
5081
}
5182

52-
claims, err := rp.VerifyIDToken(ctx, rawToken, t.verifier)
83+
// Initialize a new JWT parser with the RS256 signing method.
84+
jwtParser := jwt.NewParser(jwt.WithValidMethods([]string{"RS256"}))
85+
86+
// Parse and validate the JWT from the authentication header.
87+
token, err := jwtParser.Parse(authHeader, func(token *jwt.Token) (any, error) {
88+
// Use the JWKS from oidc to validate the JWT's signature.
89+
return oidc.JWKs.Keyfunc(token)
90+
})
5391
if err != nil {
54-
return status.Error(codes.Unauthenticated, err.Error())
92+
// Return an error if the token is invalid (e.g., expired, wrong signature).
93+
return errors.New(base.ErrorCode_ERROR_CODE_INVALID_BEARER_TOKEN.String())
94+
}
95+
96+
// Check if the parsed token is valid.
97+
if !token.Valid {
98+
// Return an error if the token is not valid.
99+
return errors.New(base.ErrorCode_ERROR_CODE_INVALID_BEARER_TOKEN.String())
100+
}
101+
102+
// Extract the claims from the token.
103+
claims, ok := token.Claims.(jwt.MapClaims)
104+
if !ok {
105+
// Return an error if the claims in the token are in an invalid format.
106+
return errors.New(base.ErrorCode_ERROR_CODE_INVALID_CLAIMS.String())
107+
}
108+
109+
// Verify the issuer of the token matches the expected issuer.
110+
if ok := claims.VerifyIssuer(oidc.IssuerURL, true); !ok {
111+
// Return an error if the token's issuer is invalid.
112+
return errors.New(base.ErrorCode_ERROR_CODE_INVALID_ISSUER.String())
55113
}
56114

57-
if err := t.validateOtherClaims(claims); err != nil {
58-
return status.Error(codes.Unauthenticated, err.Error())
115+
// Verify the audience of the token matches the expected audience.
116+
if ok := claims.VerifyAudience(oidc.Audience, true); !ok {
117+
// Return an error if the token's audience is invalid.
118+
return errors.New(base.ErrorCode_ERROR_CODE_INVALID_AUDIENCE.String())
59119
}
120+
121+
// If all checks pass, the token is considered valid, and the function returns nil.
60122
return nil
61123
}
62124

63-
// validateOtherClaims - Validate claims that are not validated by the oidc client library
64-
func (t *Authn) validateOtherClaims(claims oidc.IDTokenClaims) error {
65-
return checkNotBefore(claims, t.verifier.Offset())
125+
func (oidc *Authn) fetchKeys() error {
126+
oidcConfig, err := oidc.fetchOIDCConfiguration()
127+
if err != nil {
128+
return fmt.Errorf("error fetching OIDC configuration: %w", err)
129+
}
130+
131+
oidc.JwksURI = oidcConfig.JWKsURI
132+
133+
jwks, err := oidc.GetKeys()
134+
if err != nil {
135+
return fmt.Errorf("error fetching OIDC keys: %w", err)
136+
}
137+
138+
oidc.JWKs = jwks
139+
140+
return nil
66141
}
67142

68-
// checkNotBefore - Validate current time to be not before the notBefore claim value
69-
// Use the same logic for time comparisons as in the zitadel oidc verifier
70-
func checkNotBefore(claims oidc.IDTokenClaims, offset time.Duration) error {
71-
notBefore := claims.GetNotBefore().Round(time.Second)
72-
if notBefore.IsZero() {
73-
return nil
143+
// GetKeys fetches the JSON Web Key Set (JWKS) from the configured JWKS URI.
144+
func (oidc *Authn) GetKeys() (*keyfunc.JWKS, error) {
145+
// Use the keyfunc package to fetch the JWKS from the JWKS URI.
146+
// The keyfunc.Options struct is used to configure the HTTP client used for the request
147+
// and set a refresh interval for the keys.
148+
jwks, err := keyfunc.Get(oidc.JwksURI, keyfunc.Options{
149+
Client: oidc.httpClient, // Use the HTTP client configured in the Authn struct.
150+
RefreshInterval: 48 * time.Hour, // Set the interval to refresh the keys every 48 hours.
151+
})
152+
if err != nil {
153+
// Return a formatted error message if there's an issue fetching the JWKS.
154+
// This includes the JWKS URI for clearer debugging information.
155+
return nil, fmt.Errorf("failed to fetch keys from '%s': %s", oidc.JwksURI, err)
74156
}
75157

76-
nowWithOffset := time.Now().UTC().Add(offset).Round(time.Second)
77-
if nowWithOffset.Before(notBefore) {
78-
return fmt.Errorf("token's notBefore date is %s, should be used after that time", notBefore)
158+
// Return the fetched JWKS and nil for the error if successful.
159+
return jwks, nil
160+
}
161+
162+
// Config holds OpenID Connect (OIDC) configuration details.
163+
type Config struct {
164+
// Issuer is the OIDC provider's unique identifier URL.
165+
Issuer string `json:"issuer"`
166+
// JWKsURI is the URL to the JSON Web Key Set (JWKS) provided by the OIDC issuer.
167+
JWKsURI string `json:"jwks_uri"`
168+
}
169+
170+
// Fetches OIDC configuration using the well-known endpoint.
171+
func (oidc *Authn) fetchOIDCConfiguration() (*Config, error) {
172+
wellKnownURL := oidc.getWellKnownURL()
173+
body, err := oidc.doHTTPRequest(wellKnownURL)
174+
if err != nil {
175+
return nil, err
79176
}
80-
return nil
177+
178+
oidcConfig, err := parseOIDCConfiguration(body)
179+
if err != nil {
180+
return nil, err
181+
}
182+
183+
return oidcConfig, nil
184+
}
185+
186+
// Constructs the well-known URL for fetching OIDC configuration.
187+
func (oidc *Authn) getWellKnownURL() string {
188+
return strings.TrimSuffix(oidc.IssuerURL, "/") + "/.well-known/openid-configuration"
189+
}
190+
191+
// doHTTPRequest makes an HTTP GET request to the specified URL and returns the response body.
192+
func (oidc *Authn) doHTTPRequest(url string) ([]byte, error) {
193+
// Create a new HTTP GET request.
194+
req, err := http.NewRequest("GET", url, nil)
195+
if err != nil {
196+
// Return an error if there's an issue creating the HTTP request.
197+
return nil, fmt.Errorf("failed to create HTTP request for OIDC configuration: %s", err)
198+
}
199+
200+
// Send the request using the configured HTTP client.
201+
res, err := oidc.httpClient.Do(req)
202+
if err != nil {
203+
// Return an error if the request fails to execute.
204+
return nil, fmt.Errorf("failed to execute HTTP request for OIDC configuration: %s", err)
205+
}
206+
// Ensure the response body is closed after reading.
207+
defer res.Body.Close()
208+
209+
// Check if the HTTP status code indicates success.
210+
if res.StatusCode != http.StatusOK {
211+
// Return an error if the status code is not 200 OK.
212+
return nil, fmt.Errorf("received unexpected status code (%d) while fetching OIDC configuration", res.StatusCode)
213+
}
214+
215+
// Read the response body.
216+
body, err := io.ReadAll(res.Body)
217+
if err != nil {
218+
// Return an error if reading the response body fails.
219+
return nil, fmt.Errorf("failed to read response body from OIDC configuration request: %s", err)
220+
}
221+
222+
// Return the response body.
223+
return body, nil
224+
}
225+
226+
// parseOIDCConfiguration decodes the OIDC configuration from the given JSON body.
227+
func parseOIDCConfiguration(body []byte) (*Config, error) {
228+
var oidcConfig Config
229+
// Attempt to unmarshal the JSON body into the oidcConfig struct.
230+
if err := json.Unmarshal(body, &oidcConfig); err != nil {
231+
// Provide a specific error message indicating failure in JSON parsing.
232+
return nil, fmt.Errorf("failed to decode OIDC configuration: %s", err)
233+
}
234+
235+
// Validate that the Issuer field is not empty.
236+
if oidcConfig.Issuer == "" {
237+
// Return an error highlighting the absence of the issuer in the configuration.
238+
return nil, errors.New("issuer value is required but missing in OIDC configuration")
239+
}
240+
241+
// Validate that the JWKsURI field is not empty.
242+
if oidcConfig.JWKsURI == "" {
243+
// Return an error highlighting the absence of the jwks_uri in the configuration.
244+
return nil, errors.New("jwks_uri value is required but missing in OIDC configuration")
245+
}
246+
247+
// Return the successfully parsed configuration.
248+
return &oidcConfig, nil
249+
}
250+
251+
// Close gracefully shuts down the Authn instance by terminating background processes.
252+
func (oidc *Authn) Close() {
253+
// EndBackground stops the background refresh process for the JWKS keys,
254+
// ensuring no more go routines are left running for key refresh.
255+
oidc.JWKs.EndBackground()
81256
}

0 commit comments

Comments
 (0)