Passed
Push — master ( 8b6a99...606072 )
by Tolga
01:20 queued 16s
created

oidc.*Authn.GetKeys   A

Complexity

Conditions 2

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nop 0
dl 0
loc 16
rs 10
c 0
b 0
f 0
1
package oidc
2
3
import (
4
	"context"
5
	"encoding/json"
6
	"errors"
7
	"fmt"
8
	"io"
9
	"net/http"
10
	"strings"
11
	"time"
12
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"
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
// Authn holds configuration for OIDC authentication, including issuer, audience, and key details.
28
type Authn struct {
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
43
}
44
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()
65
	if err != nil {
66
		// If fetching keys fails, return an error to prevent initialization of a non-functional Authn instance.
67
		return nil, err
68
	}
69
70
	// Return the initialized Authn instance, ready for use in OIDC authentication.
71
	return oidc, nil
72
}
73
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")
78
	if err != nil {
79
		// Return an error if the bearer token is missing from the authentication header.
80
		return errors.New(base.ErrorCode_ERROR_CODE_MISSING_BEARER_TOKEN.String())
81
	}
82
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
	})
91
	if err != nil {
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())
113
	}
114
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())
119
	}
120
121
	// If all checks pass, the token is considered valid, and the function returns nil.
122
	return nil
123
}
124
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)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
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)
0 ignored issues
show
introduced by
unrecognized printf verb 'w'
Loading history...
136
	}
137
138
	oidc.JWKs = jwks
139
140
	return nil
141
}
142
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)
156
	}
157
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
176
	}
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()
256
}
257