Passed
Push — master ( d91163...14ee35 )
by Tolga
01:24 queued 15s
created

oidc.NewOidcAuthn   A

Complexity

Conditions 3

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 19
nop 2
dl 0
loc 35
rs 9.45
c 0
b 0
f 0
1
package oidc
2
3
import (
4
	"context"
5
	"encoding/json"
6
	"errors"
7
	"fmt"
8
	"io"
9
	"log/slog"
10
	"net/http"
11
	"strings"
12
13
	"github.com/golang-jwt/jwt/v4"
14
	grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
15
	"github.com/hashicorp/go-retryablehttp"
16
	"github.com/lestrrat-go/jwx/jwk"
17
18
	"github.com/Permify/permify/internal/config"
19
	base "github.com/Permify/permify/pkg/pb/base/v1"
20
)
21
22
// Authenticator - Interface for oidc authenticator
23
type Authenticator interface {
24
	Authenticate(ctx context.Context) error
25
}
26
27
type Authn struct {
28
	// URL of the issuer. This is typically the base URL of the identity provider.
29
	IssuerURL string
30
	// Audience for which the token is intended. It must match the audience in the JWT.
31
	Audience string
32
	// URL of the JSON Web Key Set (JWKS). This URL hosts public keys used to verify JWT signatures.
33
	JwksURI string
34
	// Pointer to an AutoRefresh object from the JWKS library. It helps in automatically refreshing the JWKS at predefined intervals.
35
	jwksSet *jwk.AutoRefresh
36
	// List of valid signing methods. Specifies which signing algorithms are considered valid for the JWTs.
37
	validMethods []string
38
	// Pointer to a JWT parser object. This is used to parse and validate the JWT tokens.
39
	jwtParser *jwt.Parser
40
}
41
42
// NewOidcAuthn initializes a new instance of the Authn struct with OpenID Connect (OIDC) configuration.
43
// It takes in a context for managing cancellation and a configuration object. It returns a pointer to an Authn instance or an error.
44
func NewOidcAuthn(ctx context.Context, conf config.Oidc) (*Authn, error) {
45
	// Create a new HTTP client with retry capabilities. This client is used for making HTTP requests, particularly for fetching OIDC configuration.
46
	client := retryablehttp.NewClient()
47
	client.Logger = nil // Disable logging for the HTTP client to avoid noisy logs.
48
49
	// Fetch the OIDC configuration from the issuer's well-known configuration endpoint.
50
	oidcConf, err := fetchOIDCConfiguration(client.StandardClient(), strings.TrimSuffix(conf.Issuer, "/")+"/.well-known/openid-configuration")
51
	if err != nil {
52
		// If there is an error fetching the OIDC configuration, return nil and the error.
53
		return nil, fmt.Errorf("failed to fetch OIDC configuration: %w", err)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
54
	}
55
56
	// Set up automatic refresh of the JSON Web Key Set (JWKS) to ensure the public keys are always up-to-date.
57
	ar := jwk.NewAutoRefresh(ctx)                                                                                              // Create a new AutoRefresh instance for the JWKS.
58
	ar.Configure(oidcConf.JWKsURI, jwk.WithHTTPClient(client.StandardClient()), jwk.WithRefreshInterval(conf.RefreshInterval)) // Configure the auto-refresh parameters.
59
60
	// Initialize the Authn struct with the OIDC configuration details and other relevant settings.
61
	oidc := &Authn{
62
		IssuerURL:    conf.Issuer,                                            // URL of the token issuer.
63
		Audience:     conf.Audience,                                          // Intended audience of the token.
64
		JwksURI:      oidcConf.JWKsURI,                                       // URL of the JWKS endpoint.
65
		validMethods: conf.ValidMethods,                                      // List of acceptable signing methods for the tokens.
66
		jwtParser:    jwt.NewParser(jwt.WithValidMethods(conf.ValidMethods)), // JWT parser configured with the valid signing methods.
67
		jwksSet:      ar,                                                     // Set the JWKS auto-refresh instance.
68
	}
69
70
	// Attempt to fetch the JWKS immediately to ensure it's available and valid.
71
	_, err = oidc.jwksSet.Fetch(ctx, oidc.JwksURI)
72
	if err != nil {
73
		// If there is an error fetching the JWKS, return nil and the error.
74
		return nil, fmt.Errorf("failed to fetch JWKS: %w", err)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
75
	}
76
77
	// Return the initialized OIDC authentication object and no error.
78
	return oidc, nil
79
}
80
81
// Authenticate validates the JWT token found in the authorization header of the incoming request.
82
// It uses the OIDC configuration to validate the token against the issuer's public keys.
83
func (oidc *Authn) Authenticate(requestContext context.Context) error {
84
	// Extract the authorization header from the metadata of the incoming gRPC request.
85
	authHeader, err := grpcauth.AuthFromMD(requestContext, "Bearer")
86
	if err != nil {
87
		// Log the error if the authorization header is missing or does not start with "Bearer"
88
		slog.Error("failed to extract authorization header from gRPC request", "error", err)
89
		// Return an error indicating the missing or incorrect bearer token
90
		return errors.New(base.ErrorCode_ERROR_CODE_MISSING_BEARER_TOKEN.String())
91
	}
92
93
	// Log the successful extraction of the authorization header for debugging purposes.
94
	// Remember, do not log the actual content of authHeader as it might contain sensitive information.
95
	slog.Debug("Successfully extracted authorization header from gRPC request")
96
97
	// Parse and validate the JWT token extracted from the authorization header.
98
	parsedToken, err := oidc.jwtParser.Parse(authHeader, func(token *jwt.Token) (interface{}, error) {
99
		slog.Info("starting JWT parsing and validation.")
100
101
		// Fetch the public keys from the JWKS endpoint configured for the OIDC.
102
		jwks, err := oidc.jwksSet.Fetch(requestContext, oidc.JwksURI)
103
		if err != nil {
104
			slog.Error("failed to fetch JWKS", "uri", oidc.JwksURI, "error", err)
105
			// If fetching the JWKS fails, return an error.
106
			return nil, fmt.Errorf("failed to fetch JWKS: %w", err)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
107
		}
108
109
		slog.Debug("successfully fetched JWKS", "jwks", jwks)
110
111
		// Retrieve the key ID from the JWT header and find the corresponding key in the JWKS.
112
		if keyID, ok := token.Header["kid"].(string); ok {
113
			slog.Debug("attempting to find key in JWKS", "kid", keyID)
114
115
			if key, found := jwks.LookupKeyID(keyID); found {
116
				// If the key is found, convert it to a usable format.
117
				var k interface{}
118
				if err := key.Raw(&k); err != nil {
119
					slog.Error("failed to get raw public key", "kid", keyID, "error", err)
120
					return nil, fmt.Errorf("failed to get raw public key: %w", err)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
121
				}
122
				slog.Debug("successfully obtained raw public key", "key", k)
123
				return k, nil // Return the public key for JWT signature verification.
124
			}
125
			slog.Error("key ID not found in JWKS", "kid", keyID)
126
			// If the specified key ID is not found in the JWKS, return an error.
127
			return nil, fmt.Errorf("kid %s not found", keyID)
128
		}
129
		slog.Error("jwt does not contain a key ID")
130
		// If the JWT does not contain a key ID, return an error.
131
		return nil, errors.New("kid must be specified in the token header")
132
	})
133
	if err != nil {
134
		// Log that the token parsing or validation failed
135
		slog.Error("token parsing or validation failed", "error", err)
136
		// If token parsing or validation fails, return an error indicating the token is invalid.
137
		return errors.New(base.ErrorCode_ERROR_CODE_INVALID_BEARER_TOKEN.String())
138
	}
139
140
	// Ensure the token is valid.
141
	if !parsedToken.Valid {
142
		// Log that the parsed token was not valid
143
		slog.Warn("parsed token is invalid")
144
		// Return an error indicating the invalid token
145
		return errors.New(base.ErrorCode_ERROR_CODE_INVALID_BEARER_TOKEN.String())
146
	}
147
148
	// Extract the claims from the token.
149
	claims, ok := parsedToken.Claims.(jwt.MapClaims)
150
	if !ok {
151
		// Log that the claims were in an incorrect format
152
		slog.Warn("token claims are in an incorrect format")
153
		// Return an error
154
		return errors.New(base.ErrorCode_ERROR_CODE_INVALID_CLAIMS.String())
155
	}
156
157
	slog.Debug("extracted token claims", "claims", claims)
158
159
	// Verify the issuer of the token matches the expected issuer.
160
	if ok := claims.VerifyIssuer(oidc.IssuerURL, true); !ok {
161
		// Log that the issuer did not match the expected issuer
162
		slog.Warn("token issuer is invalid", "expected", oidc.IssuerURL, "actual", claims["iss"])
163
		// Return an error
164
		return errors.New(base.ErrorCode_ERROR_CODE_INVALID_ISSUER.String())
165
	}
166
167
	// Verify the audience of the token matches the expected audience.
168
	if ok := claims.VerifyAudience(oidc.Audience, true); !ok {
169
		// Log that the audience did not match the expected audience
170
		slog.Warn("token audience is invalid", "expected", oidc.Audience, "actual", claims["aud"])
171
		// Return an error
172
		return errors.New(base.ErrorCode_ERROR_CODE_INVALID_AUDIENCE.String())
173
	}
174
175
	// If all validations pass, return nil indicating the token is valid.
176
	return nil
177
}
178
179
// Config holds OpenID Connect (OIDC) configuration details.
180
type Config struct {
181
	// Issuer is the OIDC provider's unique identifier URL.
182
	Issuer string `json:"issuer"`
183
	// JWKsURI is the URL to the JSON Web Key Set (JWKS) provided by the OIDC issuer.
184
	JWKsURI string `json:"jwks_uri"`
185
}
186
187
// fetchOIDCConfiguration sends an HTTP request to the given URL to fetch the OpenID Connect (OIDC) configuration.
188
// It requires an HTTP client and the URL from which to fetch the configuration.
189
func fetchOIDCConfiguration(client *http.Client, url string) (*Config, error) {
190
	// Send an HTTP GET request to the provided URL to fetch the OIDC configuration.
191
	// This typically points to the well-known configuration endpoint of the OIDC provider.
192
	body, err := doHTTPRequest(client, url)
193
	if err != nil {
194
		// If there is an error in fetching the configuration (network error, bad response, etc.), return nil and the error.
195
		return nil, err
196
	}
197
198
	// Parse the JSON response body into an OIDC Config struct.
199
	// This involves unmarshalling the JSON into a struct that matches the expected fields of the OIDC configuration.
200
	oidcConfig, err := parseOIDCConfiguration(body)
201
	if err != nil {
202
		// If there is an error in parsing the JSON response (missing fields, incorrect format, etc.), return nil and the error.
203
		return nil, err
204
	}
205
206
	// Return the parsed OIDC configuration and nil as the error (indicating success).
207
	return oidcConfig, nil
208
}
209
210
// doHTTPRequest makes an HTTP GET request to the specified URL and returns the response body.
211
func doHTTPRequest(client *http.Client, url string) ([]byte, error) {
212
	// Log the attempt to create a new HTTP GET request
213
	slog.Debug("creating new HTTP GET request", "url", url)
214
215
	// Create a new HTTP GET request.
216
	req, err := http.NewRequest("GET", url, nil)
217
	if err != nil {
218
		// Log the error if creating the HTTP request fails
219
		slog.Error("failed to create HTTP request", "url", url, "error", err)
220
		return nil, fmt.Errorf("failed to create HTTP request for OIDC configuration: %s", err)
221
	}
222
223
	// Log the execution of the HTTP request
224
	slog.Debug("executing HTTP request", "url", url)
225
226
	// Send the request using the configured HTTP client.
227
	res, err := client.Do(req)
228
	if err != nil {
229
		// Log the error if executing the HTTP request fails
230
		slog.Error("failed to execute HTTP request", "url", url, "error", err)
231
		return nil, fmt.Errorf("failed to execute HTTP request for OIDC configuration: %s", err)
232
	}
233
234
	// Log the HTTP status code of the response
235
	slog.Debug("received HTTP response", "status_code", res.StatusCode, "url", url)
236
237
	// Ensure the response body is closed after reading.
238
	defer res.Body.Close()
239
240
	// Check if the HTTP status code indicates success.
241
	if res.StatusCode != http.StatusOK {
242
		// Log the unexpected status code
243
		slog.Warn("received unexpected status code", "status_code", res.StatusCode, "url", url)
244
		return nil, fmt.Errorf("received unexpected status code (%d) while fetching OIDC configuration", res.StatusCode)
245
	}
246
247
	// Log the attempt to read the response body
248
	slog.Debug("reading response body", "url", url)
249
250
	// Read the response body.
251
	body, err := io.ReadAll(res.Body)
252
	if err != nil {
253
		// Log the error if reading the response body fails
254
		slog.Error("failed to read response body", "url", url, "error", err)
255
		return nil, fmt.Errorf("failed to read response body from OIDC configuration request: %s", err)
256
	}
257
258
	// Log the successful retrieval of the response body
259
	slog.Debug("successfully read response body", "url", url, "response_length", len(body))
260
261
	// Return the response body.
262
	return body, nil
263
}
264
265
// parseOIDCConfiguration decodes the OIDC configuration from the given JSON body.
266
func parseOIDCConfiguration(body []byte) (*Config, error) {
267
	var oidcConfig Config
268
	// Attempt to unmarshal the JSON body into the oidcConfig struct.
269
	if err := json.Unmarshal(body, &oidcConfig); err != nil {
270
		// Log the error if unmarshalling
271
		slog.Error("failed to unmarshal OIDC configuration", "error", err)
272
		return nil, fmt.Errorf("failed to decode OIDC configuration: %s", err)
273
	}
274
	// Log the successful decoding of OIDC configuration
275
	slog.Debug("successfully decoded OIDC configuration")
276
277
	if oidcConfig.Issuer == "" {
278
		// Log missing issuer value
279
		slog.Warn("missing issuer value in OIDC configuration")
280
		return nil, errors.New("issuer value is required but missing in OIDC configuration")
281
	}
282
283
	if oidcConfig.JWKsURI == "" {
284
		// Log missing JWKsURI value
285
		slog.Warn("missing JWKsURI value in OIDC configuration")
286
		return nil, errors.New("JWKsURI value is required but missing in OIDC configuration")
287
	}
288
289
	// Log the successful parsing of the OIDC configuration
290
	slog.Info("successfully parsed OIDC configuration", "issuer", oidcConfig.Issuer, "jwks_uri", oidcConfig.JWKsURI)
291
292
	// Return the successfully parsed configuration.
293
	return &oidcConfig, nil
294
}
295