Skip to content

Commit 63dce14

Browse files
authored
fix: remove azure claim overage code. (#2005)
Won't be supporting this.
1 parent 44890d0 commit 63dce14

3 files changed

Lines changed: 107 additions & 356 deletions

File tree

internal/api/provider/azure.go

Lines changed: 0 additions & 210 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,10 @@ import (
55
"encoding/base64"
66
"encoding/json"
77
"fmt"
8-
"io"
9-
"net/http"
10-
"net/url"
118
"regexp"
129
"strings"
13-
"unicode/utf8"
1410

1511
"github.com/coreos/go-oidc/v3/oidc"
16-
"github.com/golang-jwt/jwt/v5"
1712
"github.com/supabase/auth/internal/conf"
1813
"golang.org/x/oauth2"
1914
)
@@ -167,208 +162,3 @@ func (g azureProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use
167162

168163
return nil, fmt.Errorf("azure: no OIDC ID token present in response")
169164
}
170-
171-
type AzureIDTokenClaimSource struct {
172-
Endpoint string `json:"endpoint"`
173-
}
174-
175-
type AzureIDTokenClaims struct {
176-
jwt.RegisteredClaims
177-
178-
Email string `json:"email"`
179-
Name string `json:"name"`
180-
PreferredUsername string `json:"preferred_username"`
181-
XMicrosoftEmailDomainOwnerVerified any `json:"xms_edov"`
182-
183-
ClaimNames map[string]string `json:"_claim_names"`
184-
ClaimSources map[string]AzureIDTokenClaimSource `json:"_claim_sources"`
185-
}
186-
187-
// ResolveIndirectClaims resolves claims in the Azure Token that require a call to the Microsoft Graph API. This is typically to an API like this: https://learn.microsoft.com/en-us/graph/api/directoryobject-getmemberobjects?view=graph-rest-1.0&tabs=http
188-
func (c *AzureIDTokenClaims) ResolveIndirectClaims(ctx context.Context, httpClient *http.Client, accessToken string) (map[string]any, error) {
189-
if len(c.ClaimNames) == 0 || len(c.ClaimSources) == 0 {
190-
return nil, nil
191-
}
192-
193-
result := make(map[string]any)
194-
195-
for claimName, claimSource := range c.ClaimNames {
196-
claimEndpointObject, ok := c.ClaimSources[claimSource]
197-
198-
if !ok || !strings.HasPrefix(claimEndpointObject.Endpoint, "https://") {
199-
continue
200-
}
201-
202-
u, err := url.ParseRequestURI(claimEndpointObject.Endpoint)
203-
if err != nil {
204-
return nil, fmt.Errorf("azure: failed to parse endpoint URL %q (resolving overage claim %q): %w", claimEndpointObject.Endpoint, claimName, err)
205-
}
206-
207-
queryParams := u.Query()
208-
if !queryParams.Has("api-version") {
209-
// https://stackoverflow.com/questions/51085863/retrieve-group-claims-using-claim-sources-returns-the-specified-api-version-is
210-
queryParams.Add("api-version", "1.6")
211-
u.RawQuery = queryParams.Encode()
212-
}
213-
214-
claimEndpoint := u.String()
215-
216-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, claimEndpoint, strings.NewReader(`{"securityEnabledOnly":true}`))
217-
if err != nil {
218-
return nil, fmt.Errorf("azure: failed to create POST request to %q (resolving overage claim %q): %w", claimEndpoint, claimName, err)
219-
}
220-
221-
req.Header.Add("Authorization", "Bearer "+accessToken)
222-
req.Header.Add("Content-Type", "application/json")
223-
224-
resp, err := httpClient.Do(req)
225-
if err != nil {
226-
return nil, fmt.Errorf("azure: failed to send POST request to %q (resolving overage claim %q): %w", claimEndpoint, claimName, err)
227-
}
228-
229-
defer resp.Body.Close()
230-
231-
if resp.StatusCode != http.StatusOK {
232-
resBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 2*1024))
233-
234-
body := "<empty>"
235-
if len(resBody) > 0 {
236-
if utf8.Valid(resBody) {
237-
body = string(resBody)
238-
} else {
239-
body = "<invalid-utf8>"
240-
}
241-
}
242-
243-
readErrString := ""
244-
if readErr != nil {
245-
readErrString = fmt.Sprintf(" with read error %q", readErr.Error())
246-
}
247-
248-
return nil, fmt.Errorf("azure: received %d but expected 200 HTTP status code when sending POST to %q (resolving overage claim %q) with response body %q%s", resp.StatusCode, claimEndpoint, claimName, body, readErrString)
249-
}
250-
251-
var responseResult struct {
252-
Value any `json:"value"`
253-
}
254-
255-
if err := json.NewDecoder(resp.Body).Decode(&responseResult); err != nil {
256-
return nil, fmt.Errorf("azure: failed to parse JSON response from POST to %q (resolving overage claim %q): %w", claimEndpoint, claimName, err)
257-
}
258-
259-
result[claimName] = responseResult.Value
260-
}
261-
262-
return result, nil
263-
}
264-
265-
func (c *AzureIDTokenClaims) IsEmailVerified() bool {
266-
emailVerified := false
267-
268-
edov := c.XMicrosoftEmailDomainOwnerVerified
269-
270-
// If xms_edov is not set, and an email is present or xms_edov is true,
271-
// only then is the email regarded as verified.
272-
// https://learn.microsoft.com/en-us/azure/active-directory/develop/migrate-off-email-claim-authorization#using-the-xms_edov-optional-claim-to-determine-email-verification-status-and-migrate-users
273-
if edov == nil {
274-
// An email is provided, but xms_edov is not -- probably not
275-
// configured, so we must assume the email is verified as Azure
276-
// will only send out a potentially unverified email address in
277-
// single-tenanat apps.
278-
emailVerified = c.Email != ""
279-
} else {
280-
edovBool := false
281-
282-
// Azure can't be trusted with how they encode the xms_edov
283-
// claim. Sometimes it's "xms_edov": "1", sometimes "xms_edov": true.
284-
switch v := edov.(type) {
285-
case bool:
286-
edovBool = v
287-
288-
case string:
289-
edovBool = v == "1" || v == "true"
290-
291-
default:
292-
edovBool = false
293-
}
294-
295-
emailVerified = c.Email != "" && edovBool
296-
}
297-
298-
return emailVerified
299-
}
300-
301-
// removeAzureClaimsFromCustomClaims contains the list of claims to be removed
302-
// from the CustomClaims map. See:
303-
// https://learn.microsoft.com/en-us/azure/active-directory/develop/id-token-claims-reference
304-
var removeAzureClaimsFromCustomClaims = []string{
305-
"aud",
306-
"iss",
307-
"iat",
308-
"nbf",
309-
"exp",
310-
"c_hash",
311-
"at_hash",
312-
"aio",
313-
"nonce",
314-
"rh",
315-
"uti",
316-
"jti",
317-
"ver",
318-
"sub",
319-
"name",
320-
"preferred_username",
321-
}
322-
323-
func parseAzureIDToken(ctx context.Context, token *oidc.IDToken, accessToken string) (*oidc.IDToken, *UserProvidedData, error) {
324-
var data UserProvidedData
325-
326-
var azureClaims AzureIDTokenClaims
327-
if err := token.Claims(&azureClaims); err != nil {
328-
return nil, nil, err
329-
}
330-
331-
data.Metadata = &Claims{
332-
Issuer: token.Issuer,
333-
Subject: token.Subject,
334-
ProviderId: token.Subject,
335-
PreferredUsername: azureClaims.PreferredUsername,
336-
FullName: azureClaims.Name,
337-
CustomClaims: make(map[string]any),
338-
}
339-
340-
if azureClaims.Email != "" {
341-
data.Emails = []Email{{
342-
Email: azureClaims.Email,
343-
Verified: azureClaims.IsEmailVerified(),
344-
Primary: true,
345-
}}
346-
}
347-
348-
if err := token.Claims(&data.Metadata.CustomClaims); err != nil {
349-
return nil, nil, err
350-
}
351-
352-
resolvedClaims, err := azureClaims.ResolveIndirectClaims(ctx, http.DefaultClient, accessToken)
353-
if err != nil {
354-
return nil, nil, err
355-
}
356-
357-
if data.Metadata.CustomClaims == nil {
358-
if resolvedClaims != nil {
359-
data.Metadata.CustomClaims = make(map[string]any, len(resolvedClaims))
360-
}
361-
}
362-
363-
if data.Metadata.CustomClaims != nil {
364-
for _, claim := range removeAzureClaimsFromCustomClaims {
365-
delete(data.Metadata.CustomClaims, claim)
366-
}
367-
}
368-
369-
for k, v := range resolvedClaims {
370-
data.Metadata.CustomClaims[k] = v
371-
}
372-
373-
return token, &data, nil
374-
}
Lines changed: 1 addition & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
package provider
22

3-
import (
4-
"context"
5-
"net/http"
6-
"net/http/httptest"
7-
"net/url"
8-
"strings"
9-
"testing"
10-
11-
"github.com/stretchr/testify/require"
12-
)
3+
import "testing"
134

145
func TestIsAzureIssuer(t *testing.T) {
156
positiveExamples := []string{
@@ -36,138 +27,3 @@ func TestIsAzureIssuer(t *testing.T) {
3627
}
3728
}
3829
}
39-
40-
func TestAzureResolveIndirectClaims(t *testing.T) {
41-
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
42-
w.Header().Add("Content-Type", "application/json")
43-
w.WriteHeader(http.StatusOK)
44-
45-
w.Write([]byte(`{
46-
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(Edm.String)",
47-
"value": [
48-
"fee2c45b-915a-4a64-b130-f4eb9e75525e",
49-
"4fe90ae7-065a-478b-9400-e0a0e1cbd540",
50-
"c9ee2d50-9e8a-4352-b97c-4c2c99557c22",
51-
"e0c3beaf-eeb4-43d8-abc5-94f037a65697"
52-
]
53-
}`))
54-
}))
55-
56-
defer server.Close()
57-
58-
var claims AzureIDTokenClaims
59-
60-
resolvedClaims, err := claims.ResolveIndirectClaims(context.Background(), server.Client(), "access-token")
61-
require.Nil(t, resolvedClaims)
62-
require.Nil(t, err)
63-
64-
claims.ClaimNames = make(map[string]string)
65-
66-
resolvedClaims, err = claims.ResolveIndirectClaims(context.Background(), server.Client(), "access-token")
67-
require.Nil(t, resolvedClaims)
68-
require.Nil(t, err)
69-
70-
claims.ClaimNames = map[string]string{
71-
"groups": "src1",
72-
"missing-source": "src2",
73-
"not-https": "src3",
74-
}
75-
claims.ClaimSources = map[string]AzureIDTokenClaimSource{
76-
"src1": {
77-
Endpoint: server.URL,
78-
},
79-
"src3": {
80-
Endpoint: "http://example.com",
81-
},
82-
}
83-
84-
resolvedClaims, err = claims.ResolveIndirectClaims(context.Background(), server.Client(), "access-token")
85-
require.NoError(t, err)
86-
require.NotNil(t, resolvedClaims)
87-
require.Equal(t, 1, len(resolvedClaims))
88-
require.Equal(t, 4, len(resolvedClaims["groups"].([]interface{})))
89-
}
90-
91-
func TestAzureResolveIndirectClaimsFailures(t *testing.T) {
92-
examples := []struct {
93-
name string
94-
urlSuffix string
95-
statusCode int
96-
body []byte
97-
expectedError string
98-
}{
99-
{
100-
name: "invalid url",
101-
urlSuffix: "\000",
102-
expectedError: "azure: failed to parse endpoint URL \"SERVER-URL\\x00\" (resolving overage claim \"groups\"): parse \"SERVER-URL\\x00\": net/url: invalid control character in URL",
103-
},
104-
{
105-
name: "no such server",
106-
urlSuffix: "000",
107-
expectedError: "azure: failed to send POST request to \"SERVER-URL000\" (resolving overage claim \"groups\"): Post \"SERVER-URL000\": dial tcp: address PORT000: invalid port",
108-
},
109-
{
110-
name: "non 200 status code",
111-
statusCode: 500,
112-
body: []byte(`something is wrong`),
113-
expectedError: "azure: received 500 but expected 200 HTTP status code when sending POST to \"SERVER-URL\" (resolving overage claim \"groups\") with response body \"something is wrong\"",
114-
},
115-
{
116-
name: "non 200 status code, non utf8 valid body",
117-
statusCode: 201,
118-
body: []byte{255, 255, 255, 255},
119-
expectedError: "azure: received 201 but expected 200 HTTP status code when sending POST to \"SERVER-URL\" (resolving overage claim \"groups\") with response body \"<invalid-utf8>\"",
120-
},
121-
{
122-
name: "non 200 status code, empty body",
123-
statusCode: 201,
124-
body: []byte{},
125-
expectedError: "azure: received 201 but expected 200 HTTP status code when sending POST to \"SERVER-URL\" (resolving overage claim \"groups\") with response body \"<empty>\"",
126-
},
127-
{
128-
name: "non 200 status code, body over 2KB",
129-
statusCode: 201,
130-
body: []byte(strings.Repeat("x", 2*1024+1)),
131-
expectedError: "azure: received 201 but expected 200 HTTP status code when sending POST to \"SERVER-URL\" (resolving overage claim \"groups\") with response body \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"",
132-
},
133-
{
134-
name: "ok response, not json",
135-
statusCode: 200,
136-
body: []byte("not json"),
137-
expectedError: "azure: failed to parse JSON response from POST to \"SERVER-URL\" (resolving overage claim \"groups\"): invalid character 'o' in literal null (expecting 'u')",
138-
},
139-
}
140-
141-
for _, example := range examples {
142-
t.Run(example.name, func(t *testing.T) {
143-
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
144-
require.Equal(t, "1.6", r.URL.Query().Get("api-version"))
145-
146-
w.WriteHeader(example.statusCode)
147-
148-
w.Write(example.body)
149-
}))
150-
151-
defer server.Close()
152-
153-
u, _ := url.Parse(server.URL)
154-
155-
var claims AzureIDTokenClaims
156-
157-
claims.ClaimNames = map[string]string{
158-
"groups": "src1",
159-
}
160-
claims.ClaimSources = map[string]AzureIDTokenClaimSource{
161-
"src1": {
162-
Endpoint: server.URL + example.urlSuffix,
163-
},
164-
}
165-
166-
resolvedClaims, err := claims.ResolveIndirectClaims(context.Background(), server.Client(), "access-token")
167-
require.Nil(t, resolvedClaims)
168-
require.Error(t, err)
169-
require.Equal(t, example.expectedError, strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(err.Error(), server.URL, "SERVER-URL"), u.Port(), "PORT"), "?api-version=1.6", ""))
170-
})
171-
}
172-
173-
}

0 commit comments

Comments
 (0)