Skip to content

Commit 4c93c90

Browse files
sicoyleyaron2
andauthored
fix(test): up ngrok version + fix silent failure on url parsing (#3969)
Signed-off-by: Samantha Coyle <[email protected]> Co-authored-by: Yaron Schneider <[email protected]>
1 parent 6a90d6e commit 4c93c90

File tree

2 files changed

+140
-22
lines changed

2 files changed

+140
-22
lines changed

.github/scripts/components-scripts/conformance-bindings.azure.eventgrid-setup.sh

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,48 @@
22

33
set -e
44

5-
# Start ngrok
6-
wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
7-
unzip -qq ngrok-stable-linux-amd64.zip
5+
echo "Downloading ngrok..."
6+
wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz
7+
tar -xzf ngrok-v3-stable-linux-amd64.tgz
88
./ngrok authtoken ${AzureEventGridNgrokToken}
9-
./ngrok http -log=stdout --log-level debug -host-header=localhost 9000 > /tmp/ngrok.log &
9+
10+
echo "Starting ngrok tunnel..."
11+
./ngrok http --log=stdout --log-level=debug --host-header=localhost 9000 > /tmp/ngrok.log 2>&1 &
12+
NGROK_PID=$!
13+
14+
echo "Waiting for ngrok to start..."
1015
sleep 10
1116

12-
NGROK_ENDPOINT=`cat /tmp/ngrok.log | grep -Eom1 'https://.*' | sed 's/\s.*//'`
17+
# Ensure ngrok is still running
18+
if ! kill -0 $NGROK_PID 2>/dev/null; then
19+
echo "Ngrok process died. Log output:"
20+
cat /tmp/ngrok.log
21+
exit 1
22+
fi
23+
24+
echo "Getting tunnel URL from ngrok API..."
25+
MAX_RETRIES=30
26+
RETRY_COUNT=0
27+
28+
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
29+
if curl -s http://localhost:4040/api/tunnels > /tmp/ngrok_api.json 2>/dev/null; then
30+
# Extract the public URL from the API response
31+
NGROK_ENDPOINT=$(cat /tmp/ngrok_api.json | grep -o '"public_url":"[^"]*"' | head -1 | cut -d'"' -f4)
32+
33+
if [ -n "$NGROK_ENDPOINT" ] && [ "$NGROK_ENDPOINT" != "null" ]; then
34+
echo "Successfully got tunnel URL from API: $NGROK_ENDPOINT"
35+
break
36+
fi
37+
fi
38+
39+
echo "Waiting for ngrok API to be ready... (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)"
40+
sleep 2
41+
RETRY_COUNT=$((RETRY_COUNT + 1))
42+
done
43+
1344
echo "Ngrok endpoint: ${NGROK_ENDPOINT}"
1445
echo "AzureEventGridSubscriberEndpoint=${NGROK_ENDPOINT}/api/events" >> $GITHUB_ENV
46+
echo "Ngrok log:"
1547
cat /tmp/ngrok.log
1648

1749
# Schedule trigger to kill ngrok

bindings/azure/eventgrid/eventgrid.go

Lines changed: 103 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,14 @@ const (
5151
// Format for the "jwks_uri" endpoint
5252
// The %s refers to the tenant ID
5353
jwksURIFormat = "https://login.microsoftonline.com/%s/discovery/v2.0/keys"
54-
// Format for the "iss" claim in the JWT
54+
// Format for the "iss" claim in the JWT (Azure AD v2.0)
5555
// The %s refers to the tenant ID
5656
jwtIssuerFormat = "https://login.microsoftonline.com/%s/v2.0"
57+
// Format for the "iss" claim in the JWT (Azure AD v1.0 - legacy)
58+
// The %s refers to the tenant ID
59+
jwtIssuerV1Format = "https://sts.windows.net/%s/"
60+
// Eventgrid managed identity issuer
61+
eventGridIssuer = "https://eventgrid.azure.net"
5762
)
5863

5964
// AzureEventGrid allows sending/receiving Azure Event Grid events.
@@ -111,34 +116,113 @@ func (a *AzureEventGrid) Init(_ context.Context, metadata bindings.Metadata) err
111116

112117
var matchAuthHeader = regexp.MustCompile(`(?i)^(Bearer )?(([A-Za-z0-9_-]+\.){2}[A-Za-z0-9_-]+)$`)
113118

114-
func (a *AzureEventGrid) validateAuthHeader(ctx context.Context, authorizationHeader string) bool {
119+
func (a *AzureEventGrid) validateAuthHeader(ctx *fasthttp.RequestCtx) bool {
115120
// Extract the bearer token from the header
121+
authorizationHeader := string(ctx.Request.Header.Peek("authorization"))
116122
if authorizationHeader == "" {
117123
a.logger.Error("Incoming webhook request does not contain an Authorization header")
118124
return false
119125
}
126+
120127
match := matchAuthHeader.FindStringSubmatch(authorizationHeader)
121128
if len(match) < 3 {
122129
a.logger.Error("Incoming webhook request does not contain a valid bearer token in the Authorization header")
123130
return false
124131
}
125132
token := match[2]
126133

127-
// Validate the JWT
128-
_, err := jwt.ParseString(
129-
token,
130-
jwt.WithKeySet(a.jwks, jws.WithInferAlgorithmFromKey(true)),
131-
jwt.WithAudience(a.metadata.azureClientID),
132-
jwt.WithIssuer(fmt.Sprintf(jwtIssuerFormat, a.metadata.azureTenantID)),
133-
jwt.WithAcceptableSkew(5*time.Minute),
134-
jwt.WithContext(ctx),
135-
)
134+
// First, parse the JWT to see what claims we received
135+
parsedToken, err := jwt.ParseString(token, jwt.WithVerify(false))
136136
if err != nil {
137-
a.logger.Errorf("Failed to validate JWT in the incoming webhook request: %v", err)
137+
a.logger.Errorf("Failed to parse JWT: %v", err)
138138
return false
139139
}
140140

141-
return true
141+
actualIssuer := parsedToken.Issuer()
142+
azureADV2Issuer := fmt.Sprintf(jwtIssuerFormat, a.metadata.azureTenantID)
143+
expectedAudience := a.metadata.azureClientID
144+
switch actualIssuer {
145+
case azureADV2Issuer:
146+
// AzureAD v2.0 issuer
147+
_, err = jwt.ParseString(
148+
token,
149+
jwt.WithKeySet(a.jwks, jws.WithInferAlgorithmFromKey(true)),
150+
jwt.WithAudience(expectedAudience),
151+
jwt.WithIssuer(azureADV2Issuer),
152+
jwt.WithAcceptableSkew(5*time.Minute),
153+
jwt.WithContext(context.Background()),
154+
)
155+
if err == nil {
156+
return true
157+
}
158+
159+
// Also check webhook URL as audience
160+
_, err = jwt.ParseString(
161+
token,
162+
jwt.WithKeySet(a.jwks, jws.WithInferAlgorithmFromKey(true)),
163+
jwt.WithAudience(a.metadata.SubscriberEndpoint),
164+
jwt.WithIssuer(azureADV2Issuer),
165+
jwt.WithAcceptableSkew(5*time.Minute),
166+
jwt.WithContext(context.Background()),
167+
)
168+
if err == nil {
169+
return true
170+
}
171+
172+
a.logger.Errorf("JWT validation failed for AzureAD v2.0 issuer")
173+
return false
174+
175+
case fmt.Sprintf(jwtIssuerV1Format, a.metadata.azureTenantID):
176+
// AzureAD v1.0 issuer
177+
a.logger.Infof("Detected AzureAD v1.0 issuer, validating...")
178+
_, err = jwt.ParseString(
179+
token,
180+
jwt.WithKeySet(a.jwks, jws.WithInferAlgorithmFromKey(true)),
181+
jwt.WithAudience(expectedAudience),
182+
jwt.WithIssuer(actualIssuer),
183+
jwt.WithAcceptableSkew(5*time.Minute),
184+
jwt.WithContext(context.Background()),
185+
)
186+
if err == nil {
187+
return true
188+
}
189+
_, err = jwt.ParseString(
190+
token,
191+
jwt.WithKeySet(a.jwks, jws.WithInferAlgorithmFromKey(true)),
192+
jwt.WithAudience(a.metadata.SubscriberEndpoint),
193+
jwt.WithIssuer(actualIssuer),
194+
jwt.WithAcceptableSkew(5*time.Minute),
195+
jwt.WithContext(context.Background()),
196+
)
197+
if err == nil {
198+
return true
199+
}
200+
201+
a.logger.Errorf("JWT validation failed for AzureAD v1.0 issuer")
202+
return false
203+
204+
case eventGridIssuer:
205+
// eventgrid managed identity issuer - use webhook URL as audience
206+
_, err = jwt.ParseString(
207+
token,
208+
jwt.WithKeySet(a.jwks, jws.WithInferAlgorithmFromKey(true)),
209+
jwt.WithAudience(a.metadata.SubscriberEndpoint),
210+
jwt.WithIssuer(eventGridIssuer),
211+
jwt.WithAcceptableSkew(5*time.Minute),
212+
jwt.WithContext(context.Background()),
213+
)
214+
if err == nil {
215+
return true
216+
}
217+
218+
a.logger.Errorf("JWT validation failed for eventgrid issuer: %v", err)
219+
return false
220+
221+
default:
222+
a.logger.Errorf("Unexpected JWT issuer: %s. Expected either '%s', '%s', or '%s'",
223+
actualIssuer, azureADV2Issuer, fmt.Sprintf(jwtIssuerV1Format, a.metadata.azureTenantID), eventGridIssuer)
224+
return false
225+
}
142226
}
143227

144228
// Initializes the JWKS cache
@@ -284,10 +368,12 @@ func (a *AzureEventGrid) requestHandler(handler bindings.Handler) fasthttp.Reque
284368
return
285369
}
286370

287-
// Validate the Authorization header
288-
authorizationHeader := string(ctx.Request.Header.Peek("authorization"))
289-
// Note that ctx is a fasthttp context so it's actually tied to the server's lifecycle and not the request's
290-
if !a.validateAuthHeader(ctx, authorizationHeader) {
371+
// Options requests (webhook validation handshake) don't require authentication
372+
// Azure Event Grid sends options requests without authorization header during initial validation
373+
if method == http.MethodOptions {
374+
// Skip authentication for options requests
375+
} else if !a.validateAuthHeader(ctx) {
376+
// Note that ctx is a fasthttp context so it's actually tied to the server's lifecycle and not the request's
291377
ctx.Response.Header.SetStatusCode(http.StatusUnauthorized)
292378
_, err = ctx.Response.BodyWriter().Write([]byte("401 Unauthorized"))
293379
if err != nil {

0 commit comments

Comments
 (0)