@@ -0,0 +1,185 @@
1+package providers
2+
3+import (
4+ "context"
5+ "crypto/rand"
6+ "crypto/rsa"
7+ "fmt"
8+ "net/http"
9+ "net/http/httptest"
10+ "net/url"
11+ "testing"
12+
13+ "github.com/coreos/go-oidc/v3/oidc"
14+ "github.com/golang-jwt/jwt/v5"
15+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
16+ "github.com/stretchr/testify/assert"
17+
18+ . "github.com/onsi/gomega"
19+)
20+
21+func TestAzureEntraOIDCProviderNewMultiTenant(t *testing.T) {
22+ g := NewWithT(t)
23+
24+ provider := NewMicrosoftEntraIDProvider(&ProviderData{},
25+ options.Provider{OIDCConfig: options.OIDCOptions{
26+ IssuerURL: "https://login.microsoftonline.com/common/v2.0",
27+ InsecureSkipIssuerVerification: true,
28+ }},
29+ )
30+ g.Expect(provider.Data().ProviderName).To(Equal("Microsoft Entra ID"))
31+}
32+
33+func TestAzureEntraOIDCProviderNewSingleTenant(t *testing.T) {
34+ g := NewWithT(t)
35+
36+ provider := NewMicrosoftEntraIDProvider(&ProviderData{},
37+ options.Provider{OIDCConfig: options.OIDCOptions{
38+ IssuerURL: "https://login.microsoftonline.com/18014347-dd57-41a1-8191-7a1f734ea457/v2.0",
39+ }},
40+ )
41+ g.Expect(provider.Data().ProviderName).To(Equal("Microsoft Entra ID"))
42+}
43+
44+func TestAzureEntraOIDCProviderEnrichSessionGroupOverage(t *testing.T) {
45+ // Create ID Token that indicates group overage with _claim_names
46+ key, _ := rsa.GenerateKey(rand.Reader, 2048)
47+ claimsWithGroupOverage := &claimsWithGroupOverage{
48+ jwt.RegisteredClaims{
49+ Issuer: "https://login.microsoftonline.com/18014347-dd57-41a1-8191-7a1f734ea457/v2.0",
50+ },
51+ map[string]string{"groups": "src1"},
52+ }
53+
54+ jwtWithClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, claimsWithGroupOverage)
55+ signedJWT, err := jwtWithClaims.SignedString(key)
56+
57+ assert.NoError(t, err)
58+
59+ session := CreateAuthorizedSession()
60+ session.IDToken = signedJWT
61+ session.Email = "mock@example.com"
62+
63+ // Create provider
64+ provider := NewMicrosoftEntraIDProvider(&ProviderData{},
65+ options.Provider{OIDCConfig: options.OIDCOptions{
66+ IssuerURL: "https://login.microsoftonline.com/18014347-dd57-41a1-8191-7a1f734ea457/v2.0",
67+ }},
68+ )
69+
70+ // Create mocked Azure Graph server and override Graph URL
71+ mockedGraph := mockGraphAPI(false)
72+ mockedGraphURL, _ := url.Parse(mockedGraph.URL)
73+ updateURL(provider.microsoftGraphURL, mockedGraphURL.Host)
74+
75+ // Test EnrichSession
76+ err = provider.EnrichSession(context.Background(), session)
77+
78+ assert.NoError(t, err)
79+ assert.Contains(t, session.Groups, "85d7d600-7804-4d92-8d43-9c33c21c130c")
80+ assert.Contains(t, session.Groups, "916f0604-8a3b-4a69-bda9-06db11a8f0cd")
81+ assert.Contains(t, session.Groups, "b1aef995-6b55-4ac6-bbfe-e829810e9352", "Pagination using $skiptoken failed")
82+}
83+
84+func TestAzureEntraOIDCProviderValidateSessionAllowedTenants(t *testing.T) {
85+ // Create multi-tenant Azure Entra provider with allowed tenants
86+ provider := NewMicrosoftEntraIDProvider(
87+ &ProviderData{
88+ Verifier: &mockedVerifier{},
89+ },
90+ options.Provider{
91+ OIDCConfig: options.OIDCOptions{
92+ IssuerURL: "https://login.microsoftonline.com/common/v2.0",
93+ InsecureSkipIssuerVerification: true,
94+ InsecureSkipNonce: true,
95+ },
96+ MicrosoftEntraIDConfig: options.MicrosoftEntraIDOptions{
97+ AllowedTenants: []string{"85d7d600-7804-4d92-8d43-9c33c21c130c"},
98+ },
99+ },
100+ )
101+
102+ // Check for invalid tenant
103+ key, _ := rsa.GenerateKey(rand.Reader, 2048)
104+
105+ idToken := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{
106+ Issuer: "https://login.microsoftonline.com/invalid_tenant/v2.0",
107+ })
108+ invalidJWT, err := idToken.SignedString(key)
109+ assert.NoError(t, err)
110+
111+ session := CreateAuthorizedSession()
112+ session.IDToken = invalidJWT
113+
114+ valid := provider.ValidateSession(context.Background(), session)
115+ assert.False(t, valid)
116+
117+ // Check for valid tenant
118+ idToken = jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{
119+ Issuer: "https://login.microsoftonline.com/85d7d600-7804-4d92-8d43-9c33c21c130c/v2.0",
120+ })
121+ validJWT, err := idToken.SignedString(key)
122+ assert.NoError(t, err)
123+
124+ session = CreateAuthorizedSession()
125+ session.IDToken = validJWT
126+
127+ valid = provider.ValidateSession(context.Background(), session)
128+ assert.True(t, valid)
129+}
130+
131+func mockGraphAPI(noGroupMemberPermissions bool) *httptest.Server {
132+ groupsPath := "/v1.0/me/transitiveMemberOf"
133+
134+ return httptest.NewServer(http.HandlerFunc(
135+ func(w http.ResponseWriter, r *http.Request) {
136+ if noGroupMemberPermissions {
137+ w.WriteHeader(401)
138+ } else if r.URL.Path == groupsPath && r.Method == http.MethodGet && len(r.URL.Query()["$skiptoken"]) > 0 {
139+ // Second page (pagination)
140+ w.Write([]byte(`{
141+ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#directoryObjects(id)",
142+ "value": [
143+ {
144+ "@odata.type": "#microsoft.graph.group",
145+ "id": "b1aef995-6b55-4ac6-bbfe-e829810e9352"
146+ }
147+ ]
148+ }`))
149+
150+ } else if r.URL.Path == groupsPath && r.Method == http.MethodGet {
151+ // First page (pagination)
152+ w.Write([]byte(fmt.Sprintf(`{
153+ "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#directoryObjects(id)",
154+ "@odata.nextLink": "http://%s/v1.0/me/transitiveMemberOf?$select=id&$top=2&$skiptoken=TEST_TOKEN",
155+ "value": [
156+ {
157+ "@odata.type": "#microsoft.graph.group",
158+ "id": "85d7d600-7804-4d92-8d43-9c33c21c130c"
159+ },
160+ {
161+ "@odata.type": "#microsoft.graph.group",
162+ "id": "916f0604-8a3b-4a69-bda9-06db11a8f0cd"
163+ }
164+ ]
165+ }`, r.Host)))
166+ }
167+ },
168+ ))
169+}
170+
171+type claimsWithGroupOverage struct {
172+ jwt.RegisteredClaims
173+ ClaimNames interface{} `json:"_claim_names,omitempty"`
174+}
175+
176+func (c *claimsWithGroupOverage) Valid() error {
177+ return nil
178+}
179+
180+type mockedVerifier struct {
181+}
182+
183+func (v *mockedVerifier) Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error) {
184+ return nil, nil
185+}