Skip to content

Commit f5ac00e

Browse files
Merge pull request #14 from mauriciozanettisalomao/feat/lfxv2-642-auth0-jwt-sig-verification
[LFXV2-642] Auth0 - JWT verification and parsing for user authentication
2 parents 9514808 + 933c4c2 commit f5ac00e

File tree

9 files changed

+1077
-344
lines changed

9 files changed

+1077
-344
lines changed

charts/lfx-v2-auth-service/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ apiVersion: v2
55
name: lfx-v2-auth-service
66
description: LFX Platform V2 Auth Service chart
77
type: application
8-
version: 0.2.5
8+
version: 0.2.6
99
appVersion: "latest"
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
package auth0
5+
6+
import (
7+
"context"
8+
"crypto/rsa"
9+
"encoding/json"
10+
"fmt"
11+
"log/slog"
12+
"net/http"
13+
14+
"github.com/linuxfoundation/lfx-v2-auth-service/pkg/errors"
15+
"github.com/linuxfoundation/lfx-v2-auth-service/pkg/httpclient"
16+
jwtparser "github.com/linuxfoundation/lfx-v2-auth-service/pkg/jwt"
17+
)
18+
19+
// JWTVerificationConfig holds configuration for JWT signature verification
20+
type JWTVerificationConfig struct {
21+
// PublicKey is the RSA public key for signature verification
22+
PublicKey *rsa.PublicKey
23+
// ExpectedIssuer is the expected JWT issuer (e.g., "https://your-domain.auth0.com/")
24+
ExpectedIssuer string
25+
// ExpectedAudience is the expected JWT audience
26+
ExpectedAudience string
27+
// JWKSURL is the URL to fetch JSON Web Key Set (optional, alternative to PublicKey)
28+
JWKSURL string
29+
}
30+
31+
// JWTVerify verifies a JWT token with the specified required scope
32+
// https://auth0.com/docs/secure/tokens/json-web-tokens/validate-json-web-tokens
33+
func (j *JWTVerificationConfig) JWTVerify(ctx context.Context, token string, requiredScope ...string) (*jwtparser.Claims, error) {
34+
// JWT verification config is required
35+
if j == nil {
36+
return nil, errors.NewValidation("JWT verification configuration is required")
37+
}
38+
39+
// Configure JWT parsing options with signature verification
40+
opts := &jwtparser.ParseOptions{
41+
RequireExpiration: true,
42+
AllowBearerPrefix: true,
43+
RequireSubject: true,
44+
VerifySignature: true,
45+
SigningKey: j.PublicKey,
46+
ExpectedIssuer: j.ExpectedIssuer,
47+
ExpectedAudience: j.ExpectedAudience,
48+
}
49+
50+
if len(requiredScope) > 0 {
51+
opts.RequiredScopes = requiredScope
52+
}
53+
54+
// Parse and validate the JWT token with signature verification
55+
claims, err := jwtparser.ParseVerified(ctx, token, opts)
56+
if err != nil {
57+
slog.ErrorContext(ctx, "JWT signature verification failed",
58+
"error", err,
59+
"required_scope", requiredScope)
60+
return nil, err
61+
}
62+
63+
slog.DebugContext(ctx, "JWT signature verification successful",
64+
"user_id", claims.Subject,
65+
"issuer", claims.Issuer,
66+
"audience", claims.Audience,
67+
"expires_at", claims.ExpiresAt,
68+
"scope", claims.Scope,
69+
"required_scope", requiredScope)
70+
71+
return claims, nil
72+
}
73+
74+
// NewJWTVerificationConfig creates a JWT verification configuration
75+
func NewJWTVerificationConfig(ctx context.Context, domain string, httpClient *httpclient.Client) (*JWTVerificationConfig, error) {
76+
// Try to load from JWKS URL first (recommended for Auth0)
77+
jwksURL := fmt.Sprintf("https://%s/.well-known/jwks.json", domain)
78+
79+
// Fetch JWKS from Auth0 using the existing httpclient
80+
apiRequest := httpclient.NewAPIRequest(
81+
httpClient,
82+
httpclient.WithMethod(http.MethodGet),
83+
httpclient.WithURL(jwksURL),
84+
httpclient.WithDescription("fetch Auth0 JWKS"),
85+
)
86+
87+
// Parse JWKS and extract the first RSA key
88+
var jwks struct {
89+
Keys []struct {
90+
Kty string `json:"kty"`
91+
Use string `json:"use,omitempty"`
92+
Kid string `json:"kid,omitempty"`
93+
Alg string `json:"alg,omitempty"`
94+
N string `json:"n"`
95+
E string `json:"e"`
96+
} `json:"keys"`
97+
}
98+
99+
statusCode, err := apiRequest.Call(ctx, &jwks)
100+
if err != nil {
101+
return nil, errors.NewUnexpected("failed to fetch JWKS", err)
102+
}
103+
104+
if statusCode != http.StatusOK {
105+
return nil, errors.NewUnexpected(fmt.Sprintf("JWKS endpoint returned status %d", statusCode))
106+
}
107+
108+
// Find the first RSA key suitable for signature verification
109+
for _, key := range jwks.Keys {
110+
if key.Kty == "RSA" && (key.Use == "sig" || key.Use == "") {
111+
// Convert JWK to RSA public key
112+
jwkData, err := json.Marshal(key)
113+
if err != nil {
114+
continue
115+
}
116+
117+
publicKey, err := jwtparser.LoadRSAPublicKeyFromJWK(jwkData)
118+
if err != nil {
119+
return nil, errors.NewUnexpected("failed to load RSA public key from JWK", err)
120+
}
121+
122+
expectedIssuer := fmt.Sprintf("https://%s/", domain)
123+
expectedAudience := fmt.Sprintf("https://%s/api/v2/", domain)
124+
125+
slog.InfoContext(ctx, "JWT signature verification enabled",
126+
"issuer", expectedIssuer,
127+
"audience", expectedAudience,
128+
"key_id", key.Kid)
129+
130+
return &JWTVerificationConfig{
131+
PublicKey: publicKey,
132+
ExpectedIssuer: expectedIssuer,
133+
ExpectedAudience: expectedAudience,
134+
JWKSURL: jwksURL,
135+
}, nil
136+
}
137+
}
138+
139+
return nil, errors.NewUnexpected("no suitable RSA key found in JWKS for signature verification")
140+
}

0 commit comments

Comments
 (0)