Passed
Pull Request — master (#1137)
by Tolga
02:40
created

oidc.*Authn.Authenticate   D

Complexity

Conditions 12

Size

Total Lines 62
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 28
nop 1
dl 0
loc 62
rs 4.8
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like oidc.*Authn.Authenticate often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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