@@ -2,18 +2,18 @@ package oidc
22
33import (
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.
2828type 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