• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

m-lab / autojoin / 15049006145

15 May 2025 03:27PM UTC coverage: 90.783% (-1.8%) from 92.629%
15049006145

Pull #73

github

robertodauria
Change legacy_api_key var name to "key"
Pull Request #73: WIP: Support JWT authentication

3 of 32 new or added lines in 1 file covered. (9.38%)

17 existing lines in 1 file now uncovered.

1310 of 1443 relevant lines covered (90.78%)

1.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.23
/handler/validator.go
1
package handler
2

3
import (
4
        "context"
5
        "errors"
6
        "net/http"
7
        "strings"
8

9
        "github.com/golang-jwt/jwt/v4"
10
        v0 "github.com/m-lab/autojoin/api/v0"
11
        v2 "github.com/m-lab/locate/api/v2"
12
)
13

14
type contextKey string
15

16
const orgContextKey contextKey = "organization"
17

18
// APIKeyValidator is an interface for validating API keys and retrieving
19
// associated organization info.
20
type APIKeyValidator interface {
21
        // ValidateKey validates the provided API key and returns the associated
22
        // organization name if valid. Returns an error if the key is invalid.
23
        ValidateKey(ctx context.Context, key string) (string, error)
24
}
25

26
// WithAPIKeyValidation creates middleware that validates API keys and adds
27
// org info to context.
28
//
29
// Note: This supports the migration to JWT tokens. If a JWT is provided, the
30
// organization name is extracted from its claim. Otherwise, the legacy API key
31
// is validated in Datastore and the associated organization name is retrieved.
32
func WithAPIKeyValidation(validator APIKeyValidator, next http.HandlerFunc) http.HandlerFunc {
1✔
33
        return func(w http.ResponseWriter, r *http.Request) {
2✔
34
                // First, check for the Authorization header.
1✔
35
                authHeader := r.Header.Get("Authorization")
1✔
36
                if strings.HasPrefix(authHeader, "Bearer ") {
1✔
NEW
UNCOV
37
                        tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
×
NEW
UNCOV
38
                        org, err := validateJWTAndExtractOrg(tokenString)
×
NEW
UNCOV
39
                        if err != nil || org == "" {
×
NEW
UNCOV
40
                                resp := v0.RegisterResponse{
×
NEW
UNCOV
41
                                        Error: &v2.Error{
×
NEW
UNCOV
42
                                                Type:   "auth.invalid_token",
×
NEW
UNCOV
43
                                                Title:  "Invalid or missing org claim in JWT",
×
NEW
UNCOV
44
                                                Status: http.StatusUnauthorized,
×
NEW
UNCOV
45
                                        },
×
NEW
UNCOV
46
                                }
×
NEW
UNCOV
47
                                w.WriteHeader(resp.Error.Status)
×
NEW
UNCOV
48
                                writeResponse(w, resp)
×
NEW
UNCOV
49
                                return
×
NEW
UNCOV
50
                        }
×
NEW
UNCOV
51
                        ctx := context.WithValue(r.Context(), orgContextKey, org)
×
NEW
UNCOV
52
                        next.ServeHTTP(w, r.WithContext(ctx))
×
NEW
UNCOV
53
                        return
×
54
                }
55

56
                // Fallback: Use the API key from the query string, extract the organization
57
                // from Datastore.
58
                apiKey := r.URL.Query().Get("api_key")
1✔
59
                if apiKey == "" {
2✔
60
                        resp := v0.RegisterResponse{
1✔
61
                                Error: &v2.Error{
1✔
62
                                        Type:   "?api_key=<key>",
1✔
63
                                        Title:  "API key is required",
1✔
64
                                        Status: http.StatusUnauthorized,
1✔
65
                                },
1✔
66
                        }
1✔
67
                        w.WriteHeader(resp.Error.Status)
1✔
68
                        writeResponse(w, resp)
1✔
69
                        return
1✔
70
                }
1✔
71

72
                org, err := validator.ValidateKey(r.Context(), apiKey)
1✔
73
                if err != nil {
2✔
74
                        resp := v0.RegisterResponse{
1✔
75
                                Error: &v2.Error{
1✔
76
                                        Type:   "auth.invalid_key",
1✔
77
                                        Title:  "Invalid API key",
1✔
78
                                        Status: http.StatusUnauthorized,
1✔
79
                                },
1✔
80
                        }
1✔
81
                        w.WriteHeader(resp.Error.Status)
1✔
82
                        writeResponse(w, resp)
1✔
83
                        return
1✔
84
                }
1✔
85

86
                ctx := context.WithValue(r.Context(), orgContextKey, org)
1✔
87
                next.ServeHTTP(w, r.WithContext(ctx))
1✔
88
        }
89
}
90

91
// validateJWTAndExtractOrg validates the JWT and extracts the "org" claim.
NEW
92
func validateJWTAndExtractOrg(tokenString string) (string, error) {
×
NEW
93
        // Note: This JWT *must* be verified previously in the stack, e.g. via openapi
×
NEW
94
        // security definitions.
×
NEW
95
        token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
×
NEW
96
        if err != nil {
×
NEW
97
                return "", err
×
NEW
98
        }
×
NEW
99
        if claims, ok := token.Claims.(jwt.MapClaims); ok {
×
NEW
100
                if org, ok := claims["org"].(string); ok {
×
NEW
101
                        return org, nil
×
NEW
102
                }
×
103
        }
NEW
104
        return "", errors.New("org claim not found")
×
105
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc