mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-05-29 23:17:38 +02:00
Add azure groups support and oauth2 v2.0
This commit is contained in:
parent
7fe6384f38
commit
a5d918898c
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@ -20,15 +20,15 @@ jobs:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Go 1.17
|
||||
- name: Set up Go 1.18
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.17.x
|
||||
go-version: 1.18.x
|
||||
id: go
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.36.0
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.50.0
|
||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
||||
chmod +x ./cc-test-reporter
|
||||
|
||||
|
@ -8,6 +8,11 @@
|
||||
- Having a unique CSRF cookie per request can lead to quite a number of cookies, in case an application performs a high number of parallel authentication requests. Each call will redirect to /oauth2/start, if the user is not authenticated, and a new cookie will be set. The successfully authenticated requests will have its CSRF cookies immediatly expired, however the failed ones will mantain its CSRF cookies until they expire (by default in 15 minutes).
|
||||
- The user may redefine the CSRF cookie expiration time using flag "--cookie-csrf-expire" (e.g. --cookie-csrf-expire=5m). By default, it is 15 minutes, but you can fine tune to your environment.
|
||||
|
||||
- [#1574](https://github.com/oauth2-proxy/oauth2-proxy/pull/1574) Add Azure groups support and Azure OAuth v2.0 (@adriananeci)
|
||||
- group membership check is now validated while using the the azure provider.
|
||||
- Azure OAuth v2.0 (https://login.microsoftonline.com/{tenant_id}/v2.0) is now available along with Azure OAuth v1.0. See https://github.com/oauth2-proxy/oauth2-proxy/blob/master/docs/docs/configuration/auth.md#azure-auth-provider for more details
|
||||
- When using v2.0 Azure Auth endpoint (`https://login.microsoftonline.com/{tenant-id}/v2.0`) as `--oidc_issuer_url`, in conjunction with `--resource` flag, be sure to append `/.default` at the end of the resource name. See https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope for more details.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
N/A
|
||||
@ -33,6 +38,7 @@ to remain consistent with CLI flags. You should specify `code_challenge_method`
|
||||
- [#1760](https://github.com/oauth2-proxy/oauth2-proxy/pull/1760) Option to configure API routes
|
||||
- [#1825](https://github.com/oauth2-proxy/oauth2-proxy/pull/1825) Fix vulnerabilities CVE-2022-32149 and CVE-2022-27664. (@crbednarz)
|
||||
- [#1750](https://github.com/oauth2-proxy/oauth2-proxy/pull/1750) Fix Nextcloud provider
|
||||
- [#1574](https://github.com/oauth2-proxy/oauth2-proxy/pull/1574) Add Azure groups support and Azure OAuth v2.0 (@adriananeci)
|
||||
|
||||
# V7.3.0
|
||||
|
||||
|
@ -164,6 +164,7 @@ They may change between releases without notice.
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `tenant` | _string_ | Tenant directs to a tenant-specific or common (tenant-independent) endpoint<br/>Default value is 'common' |
|
||||
| `graphGroupField` | _string_ | GraphGroupField configures the group field to be used when building the groups list from Microsoft Graph<br/>Default value is 'id' |
|
||||
|
||||
### BitbucketOptions
|
||||
|
||||
|
@ -72,23 +72,45 @@ Note: The user is checked against the group members list on initial authenticati
|
||||
|
||||
### Azure Auth Provider
|
||||
|
||||
1. Add an application: go to [https://portal.azure.com](https://portal.azure.com), choose **"Azure Active Directory"** in the left menu, select **"App registrations"** and then click on **"New app registration"**.
|
||||
2. Pick a name and choose **"Webapp / API"** as application type. Use `https://internal.yourcompany.com` as Sign-on URL. Click **"Create"**.
|
||||
3. On the **"Settings"** / **"Properties"** page of the app, pick a logo and select **"Multi-tenanted"** if you want to allow users from multiple organizations to access your app. Note down the application ID. Click **"Save"**.
|
||||
4. On the **"Settings"** / **"API Permissions"** page of the app, click on **"Add a permission"**, then select **"Microsoft Graph"**, then **"Delegated permissions"** and finally check the **"openid (Sign users in)"** permission. Hit **"Save"** and then on **"Grant permissions"** (you might need another admin to do this).
|
||||
1. Add an application: go to [https://portal.azure.com](https://portal.azure.com), choose **Azure Active Directory**, select
|
||||
**App registrations** and then click on **New registration**.
|
||||
2. Pick a name, check the supported account type(single-tenant, multi-tenant, etc). In the **Redirect URI** section create a new
|
||||
**Web** platform entry for each app that you want to protect by the oauth2 proxy(e.g.
|
||||
https://internal.yourcompanycom/oauth2/callback). Click **Register**.
|
||||
3. Next we need to add group read permissions for the app registration, on the **API Permissions** page of the app, click on
|
||||
**Add a permission**, select **Microsoft Graph**, then select **Application permissions**, then click on **Group** and select
|
||||
**Group.Read.All**. Hit **Add permissions** and then on **Grant admin consent** (you might need an admin to do this).
|
||||
<br/>**IMPORTANT**: Even if this permission is listed with **"Admin consent required=No"** the consent might actually be required, due to AAD policies you won't be able to see. If you get a **"Need admin approval"** during login, most likely this is what you're missing!
|
||||
5. On the **"Settings"** / **"Reply URLs"** page of the app, add `https://internal.yourcompanycom/oauth2/callback` for each host that you want to protect by the oauth2 proxy. Click **"Save"**.
|
||||
6. On the **"Settings"** / **"Keys"** page of the app, add a new key and note down the value after hitting **"Save"**.
|
||||
7. Configure the proxy with
|
||||
4. Next, if you are planning to use v2.0 Azure Auth endpoint, go to the **Manifest** page and set `"accessTokenAcceptedVersion": 2`
|
||||
in the App registration manifest file.
|
||||
5. On the **Certificates & secrets** page of the app, add a new client secret and note down the value after hitting **Add**.
|
||||
6. Configure the proxy with:
|
||||
- for V1 Azure Auth endpoint (Azure Active Directory Endpoints - https://login.microsoftonline.com/common/oauth2/authorize)
|
||||
|
||||
```
|
||||
--provider=azure
|
||||
--client-id=<application ID from step 3>
|
||||
--client-secret=<value from step 6>
|
||||
--client-secret=<value from step 5>
|
||||
--azure-tenant={tenant-id}
|
||||
--oidc-issuer-url=https://sts.windows.net/{tenant-id}/
|
||||
```
|
||||
|
||||
Note: When using the Azure Auth provider with nginx and the cookie session store you may find the cookie is too large and doesn't get passed through correctly. Increasing the proxy_buffer_size in nginx or implementing the [redis session storage](sessions.md#redis-storage) should resolve this.
|
||||
- for V2 Azure Auth endpoint (Microsoft Identity Platform Endpoints - https://login.microsoftonline.com/common/oauth2/v2.0/authorize)
|
||||
```
|
||||
--provider=azure
|
||||
--client-id=<application ID from step 3>
|
||||
--client-secret=<value from step 5>
|
||||
--azure-tenant={tenant-id}
|
||||
--oidc-issuer-url=https://login.microsoftonline.com/{tenant-id}/v2.0
|
||||
```
|
||||
|
||||
***Notes***:
|
||||
- When using v2.0 Azure Auth endpoint (`https://login.microsoftonline.com/{tenant-id}/v2.0`) as `--oidc_issuer_url`, in conjunction
|
||||
with `--resource` flag, be sure to append `/.default` at the end of the resource name. See
|
||||
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope for more details.
|
||||
- When using the Azure Auth provider with nginx and the cookie session store you may find the cookie is too large and doesn't
|
||||
get passed through correctly. Increasing the proxy_buffer_size in nginx or implementing the [redis session storage](sessions.md#redis-storage)
|
||||
should resolve this.
|
||||
|
||||
### ADFS Auth Provider
|
||||
|
||||
|
3
go.mod
3
go.mod
@ -30,6 +30,7 @@ require (
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/vmihailenco/msgpack/v4 v4.3.11
|
||||
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
|
||||
golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
|
||||
@ -70,7 +71,7 @@ require (
|
||||
go.opentelemetry.io/otel v0.11.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
|
||||
google.golang.org/grpc v1.27.0 // indirect
|
||||
|
7
go.sum
7
go.sum
@ -132,8 +132,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
@ -318,6 +318,8 @@ golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw=
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0=
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@ -422,8 +424,9 @@ golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
|
@ -483,6 +483,7 @@ type LegacyProvider struct {
|
||||
|
||||
KeycloakGroups []string `flag:"keycloak-group" cfg:"keycloak_groups"`
|
||||
AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
|
||||
AzureGraphGroupField string `flag:"azure-graph-group-field" cfg:"azure_graph_group_field"`
|
||||
BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"`
|
||||
BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository"`
|
||||
GitHubOrg string `flag:"github-org" cfg:"github_org"`
|
||||
@ -538,6 +539,7 @@ func legacyProviderFlagSet() *pflag.FlagSet {
|
||||
|
||||
flagSet.StringSlice("keycloak-group", []string{}, "restrict logins to members of these groups (may be given multiple times)")
|
||||
flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
|
||||
flagSet.String("azure-graph-group-field", "", "configures the group field to be used when building the groups list(`id` or `displayName`. Default is `id`) from Microsoft Graph(available only for v2.0 oidc url). Based on this value, the `allowed-group` config values should be adjusted accordingly. If using `id` as group field, `allowed-group` should contains groups IDs, if using `displayName` as group field, `allowed-group` should contains groups name")
|
||||
flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
|
||||
flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")
|
||||
flagSet.String("github-org", "", "restrict logins to members of this organisation")
|
||||
@ -676,7 +678,8 @@ func (l *LegacyProvider) convert() (Providers, error) {
|
||||
// This part is out of the switch section because azure has a default tenant
|
||||
// that needs to be added from legacy options
|
||||
provider.AzureConfig = AzureOptions{
|
||||
Tenant: l.AzureTenant,
|
||||
Tenant: l.AzureTenant,
|
||||
GraphGroupField: l.AzureGraphGroupField,
|
||||
}
|
||||
|
||||
switch provider.Type {
|
||||
|
@ -142,6 +142,9 @@ type AzureOptions struct {
|
||||
// Tenant directs to a tenant-specific or common (tenant-independent) endpoint
|
||||
// Default value is 'common'
|
||||
Tenant string `json:"tenant,omitempty"`
|
||||
// GraphGroupField configures the group field to be used when building the groups list from Microsoft Graph
|
||||
// Default value is 'id'
|
||||
GraphGroupField string `json:"graphGroupField,omitempty"`
|
||||
}
|
||||
|
||||
type ADFSOptions struct {
|
||||
|
@ -149,3 +149,16 @@ func isHostnameAllowed(hostname, allowedHost string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoveDuplicateStr removes duplicates from a slice of strings.
|
||||
func RemoveDuplicateStr(strSlice []string) []string {
|
||||
allKeys := make(map[string]struct{})
|
||||
var list []string
|
||||
for _, item := range strSlice {
|
||||
if _, ok := allKeys[item]; !ok {
|
||||
allKeys[item] = struct{}{}
|
||||
list = append(list, item)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
@ -3,63 +3,60 @@ package providers
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/bitly/go-simplejson"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util"
|
||||
)
|
||||
|
||||
// AzureProvider represents an Azure based Identity Provider
|
||||
type AzureProvider struct {
|
||||
*ProviderData
|
||||
Tenant string
|
||||
Tenant string
|
||||
GraphGroupField string
|
||||
isV2Endpoint bool
|
||||
}
|
||||
|
||||
var _ Provider = (*AzureProvider)(nil)
|
||||
|
||||
const (
|
||||
azureProviderName = "Azure"
|
||||
azureDefaultScope = "openid"
|
||||
azureProviderName = "Azure"
|
||||
azureDefaultScope = "openid"
|
||||
azureDefaultGraphGroupField = "id"
|
||||
azureV2Scope = "https://graph.microsoft.com/.default"
|
||||
)
|
||||
|
||||
var (
|
||||
// Default Login URL for Azure.
|
||||
// Pre-parsed URL of https://login.microsoftonline.com/common/oauth2/authorize.
|
||||
// Default Login URL for Azure. Pre-parsed URL of https://login.microsoftonline.com/common/oauth2/authorize.
|
||||
azureDefaultLoginURL = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "login.microsoftonline.com",
|
||||
Path: "/common/oauth2/authorize",
|
||||
}
|
||||
|
||||
// Default Redeem URL for Azure.
|
||||
// Pre-parsed URL of https://login.microsoftonline.com/common/oauth2/token.
|
||||
// Default Redeem URL for Azure. Pre-parsed URL of https://login.microsoftonline.com/common/oauth2/token.
|
||||
azureDefaultRedeemURL = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "login.microsoftonline.com",
|
||||
Path: "/common/oauth2/token",
|
||||
}
|
||||
|
||||
// Default Profile URL for Azure.
|
||||
// Pre-parsed URL of https://graph.microsoft.com/v1.0/me.
|
||||
// Default Profile URL for Azure. Pre-parsed URL of https://graph.microsoft.com/v1.0/me.
|
||||
azureDefaultProfileURL = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "graph.microsoft.com",
|
||||
Path: "/v1.0/me",
|
||||
}
|
||||
|
||||
// Default ProtectedResource URL for Azure.
|
||||
// Pre-parsed URL of https://graph.microsoft.com.
|
||||
azureDefaultProtectResourceURL = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "graph.microsoft.com",
|
||||
}
|
||||
)
|
||||
|
||||
// NewAzureProvider initiates a new AzureProvider
|
||||
@ -73,9 +70,6 @@ func NewAzureProvider(p *ProviderData, opts options.AzureOptions) *AzureProvider
|
||||
scope: azureDefaultScope,
|
||||
})
|
||||
|
||||
if p.ProtectedResource == nil || p.ProtectedResource.String() == "" {
|
||||
p.ProtectedResource = azureDefaultProtectResourceURL
|
||||
}
|
||||
if p.ValidateURL == nil || p.ValidateURL.String() == "" {
|
||||
p.ValidateURL = p.ProfileURL
|
||||
}
|
||||
@ -88,9 +82,35 @@ func NewAzureProvider(p *ProviderData, opts options.AzureOptions) *AzureProvider
|
||||
overrideTenantURL(p.RedeemURL, azureDefaultRedeemURL, tenant, "token")
|
||||
}
|
||||
|
||||
graphGroupField := azureDefaultGraphGroupField
|
||||
if opts.GraphGroupField != "" {
|
||||
graphGroupField = opts.GraphGroupField
|
||||
}
|
||||
|
||||
isV2Endpoint := false
|
||||
if strings.Contains(p.LoginURL.String(), "v2.0") {
|
||||
isV2Endpoint = true
|
||||
|
||||
if strings.Contains(p.Scope, " groups") {
|
||||
logger.Print("WARNING: `groups` scope is not an accepted scope when using Azure OAuth V2 endpoint. Removing it from the scope list")
|
||||
p.Scope = strings.ReplaceAll(p.Scope, " groups", "")
|
||||
}
|
||||
|
||||
if !strings.Contains(p.Scope, " "+azureV2Scope) {
|
||||
// In order to be able to query MS Graph we must pass the ms graph default endpoint
|
||||
p.Scope += " " + azureV2Scope
|
||||
}
|
||||
|
||||
if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
|
||||
logger.Print("WARNING: `--resource` option has no effect when using the Azure OAuth V2 endpoint.")
|
||||
}
|
||||
}
|
||||
|
||||
return &AzureProvider{
|
||||
ProviderData: p,
|
||||
Tenant: tenant,
|
||||
ProviderData: p,
|
||||
Tenant: tenant,
|
||||
GraphGroupField: graphGroupField,
|
||||
isV2Endpoint: isV2Endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,8 +123,26 @@ func overrideTenantURL(current, defaultURL *url.URL, tenant, path string) {
|
||||
}
|
||||
}
|
||||
|
||||
func getMicrosoftGraphGroupsURL(graphGroupField string) *url.URL {
|
||||
|
||||
selectStatement := "$select=displayName,id"
|
||||
if !slices.Contains([]string{"displayName", "id"}, graphGroupField) {
|
||||
selectStatement += "," + graphGroupField
|
||||
}
|
||||
|
||||
// Select only security groups. Due to the filter option, count param is mandatory even if unused otherwise
|
||||
return &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "graph.microsoft.com",
|
||||
Path: "/v1.0/me/transitiveMemberOf",
|
||||
RawQuery: "$count=true&$filter=securityEnabled+eq+true&" + selectStatement,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AzureProvider) GetLoginURL(redirectURI, state, _ string, extraParams url.Values) string {
|
||||
if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
|
||||
// In azure oauth v2 there is no resource param so add it only if V1 endpoint
|
||||
// https://docs.microsoft.com/en-us/azure/active-directory/azuread-dev/azure-ad-endpoint-comparison#scopes-not-resources
|
||||
if p.ProtectedResource != nil && p.ProtectedResource.String() != "" && !p.isV2Endpoint {
|
||||
extraParams.Add("resource", p.ProtectedResource.String())
|
||||
}
|
||||
a := makeLoginURL(p.ProviderData, redirectURI, state, extraParams)
|
||||
@ -145,45 +183,39 @@ func (p *AzureProvider) Redeem(ctx context.Context, redirectURL, code, codeVerif
|
||||
session.CreatedAtNow()
|
||||
session.SetExpiresOn(time.Unix(jsonResponse.ExpiresOn, 0))
|
||||
|
||||
email, err := p.verifyTokenAndExtractEmail(ctx, session.IDToken, session.AccessToken)
|
||||
err = p.extractClaimsIntoSession(ctx, session)
|
||||
|
||||
// https://github.com/oauth2-proxy/oauth2-proxy/pull/914#issuecomment-782285814
|
||||
// https://github.com/AzureAD/azure-activedirectory-library-for-java/issues/117
|
||||
// due to above issues, id_token may not be signed by AAD
|
||||
// in that case, we will fallback to access token
|
||||
if err == nil && email != "" {
|
||||
session.Email = email
|
||||
} else {
|
||||
logger.Printf("unable to get email claim from id_token: %v", err)
|
||||
}
|
||||
|
||||
if session.Email == "" {
|
||||
email, err = p.verifyTokenAndExtractEmail(ctx, session.AccessToken, session.AccessToken)
|
||||
if err == nil && email != "" {
|
||||
session.Email = email
|
||||
} else {
|
||||
logger.Printf("unable to get email claim from access token: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get email and/or groups claims from token: %v", err)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// EnrichSession finds the email to enrich the session state
|
||||
func (p *AzureProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
|
||||
if s.Email != "" {
|
||||
return nil
|
||||
}
|
||||
// EnrichSession enriches the session state with userID, mail and groups
|
||||
func (p *AzureProvider) EnrichSession(ctx context.Context, session *sessions.SessionState) error {
|
||||
err := p.extractClaimsIntoSession(ctx, session)
|
||||
|
||||
email, err := p.getEmailFromProfileAPI(ctx, s.AccessToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get email address: %v", err)
|
||||
logger.Printf("unable to get email and/or groups claims from token: %v", err)
|
||||
}
|
||||
if email == "" {
|
||||
return errors.New("unable to get email address")
|
||||
}
|
||||
s.Email = email
|
||||
|
||||
if session.Email == "" {
|
||||
email, err := p.getEmailFromProfileAPI(ctx, session.AccessToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get email address from profile URL: %v", err)
|
||||
}
|
||||
session.Email = email
|
||||
}
|
||||
|
||||
// If using the v2.0 oidc endpoint we're also querying Microsoft Graph
|
||||
if p.isV2Endpoint {
|
||||
groups, err := p.getGroupsFromProfileAPI(ctx, session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get groups from Microsoft Graph: %v", err)
|
||||
}
|
||||
session.Groups = util.RemoveDuplicateStr(append(session.Groups, groups...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -205,33 +237,66 @@ func (p *AzureProvider) prepareRedeem(redirectURL, code, codeVerifier string) (u
|
||||
if codeVerifier != "" {
|
||||
params.Add("code_verifier", codeVerifier)
|
||||
}
|
||||
if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
|
||||
|
||||
// In azure oauth v2 there is no resource param so add it only if V1 endpoint
|
||||
// https://docs.microsoft.com/en-us/azure/active-directory/azuread-dev/azure-ad-endpoint-comparison#scopes-not-resources
|
||||
if p.ProtectedResource != nil && p.ProtectedResource.String() != "" && !p.isV2Endpoint {
|
||||
params.Add("resource", p.ProtectedResource.String())
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// verifyTokenAndExtractEmail tries to extract email claim from either id_token or access token
|
||||
// extractClaimsIntoSession tries to extract email and groups claims from either id_token or access token
|
||||
// when oidc verifier is configured
|
||||
func (p *AzureProvider) verifyTokenAndExtractEmail(ctx context.Context, rawIDToken string, accessToken string) (string, error) {
|
||||
email := ""
|
||||
func (p *AzureProvider) extractClaimsIntoSession(ctx context.Context, session *sessions.SessionState) error {
|
||||
|
||||
if rawIDToken != "" && p.Verifier != nil {
|
||||
_, err := p.Verifier.Verify(ctx, rawIDToken)
|
||||
// due to issues mentioned above, id_token may not be signed by AAD
|
||||
if err == nil {
|
||||
s, err := p.buildSessionFromClaims(rawIDToken, accessToken)
|
||||
if err == nil {
|
||||
email = s.Email
|
||||
} else {
|
||||
logger.Printf("unable to get claims from token: %v", err)
|
||||
}
|
||||
} else {
|
||||
logger.Printf("unable to verify token: %v", err)
|
||||
}
|
||||
var s *sessions.SessionState
|
||||
|
||||
// First let's verify session token
|
||||
if err := p.verifySessionToken(ctx, session); err != nil {
|
||||
return fmt.Errorf("unable to verify token: %v", err)
|
||||
}
|
||||
|
||||
return email, nil
|
||||
// https://github.com/oauth2-proxy/oauth2-proxy/pull/914#issuecomment-782285814
|
||||
// https://github.com/AzureAD/azure-activedirectory-library-for-java/issues/117
|
||||
// due to above issues, id_token may not be signed by AAD
|
||||
// in that case, we will fallback to access token
|
||||
var err error
|
||||
s, err = p.buildSessionFromClaims(session.IDToken, session.AccessToken)
|
||||
if err != nil || s.Email == "" {
|
||||
s, err = p.buildSessionFromClaims(session.AccessToken, session.AccessToken)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get claims from token: %v", err)
|
||||
}
|
||||
|
||||
session.Email = s.Email
|
||||
if s.Groups != nil {
|
||||
session.Groups = s.Groups
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifySessionToken tries to validate id_token if present or access token when oidc verifier is configured
|
||||
func (p *AzureProvider) verifySessionToken(ctx context.Context, session *sessions.SessionState) error {
|
||||
// Without a verifier there's no way to verify
|
||||
if p.Verifier == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if session.IDToken != "" {
|
||||
if _, err := p.Verifier.Verify(ctx, session.IDToken); err != nil {
|
||||
logger.Printf("unable to verify ID token, fallback to access token: %v", err)
|
||||
if _, err = p.Verifier.Verify(ctx, session.AccessToken); err != nil {
|
||||
return fmt.Errorf("unable to verify access token: %v", err)
|
||||
}
|
||||
}
|
||||
} else if _, err := p.Verifier.Verify(ctx, session.AccessToken); err != nil {
|
||||
return fmt.Errorf("unable to verify access token: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshSession uses the RefreshToken to fetch new Access and ID Tokens
|
||||
@ -285,25 +350,10 @@ func (p *AzureProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sess
|
||||
s.CreatedAtNow()
|
||||
s.SetExpiresOn(time.Unix(jsonResponse.ExpiresOn, 0))
|
||||
|
||||
email, err := p.verifyTokenAndExtractEmail(ctx, s.IDToken, s.AccessToken)
|
||||
err = p.extractClaimsIntoSession(ctx, s)
|
||||
|
||||
// https://github.com/oauth2-proxy/oauth2-proxy/pull/914#issuecomment-782285814
|
||||
// https://github.com/AzureAD/azure-activedirectory-library-for-java/issues/117
|
||||
// due to above issues, id_token may not be signed by AAD
|
||||
// in that case, we will fallback to access token
|
||||
if err == nil && email != "" {
|
||||
s.Email = email
|
||||
} else {
|
||||
logger.Printf("unable to get email claim from id_token: %v", err)
|
||||
}
|
||||
|
||||
if s.Email == "" {
|
||||
email, err = p.verifyTokenAndExtractEmail(ctx, s.AccessToken, s.AccessToken)
|
||||
if err == nil && email != "" {
|
||||
s.Email = email
|
||||
} else {
|
||||
logger.Printf("unable to get email claim from access token: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Printf("unable to get email and/or groups claims from token: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -313,11 +363,75 @@ func makeAzureHeader(accessToken string) http.Header {
|
||||
return makeAuthorizationHeader(tokenTypeBearer, accessToken, nil)
|
||||
}
|
||||
|
||||
func getEmailFromJSON(json *simplejson.Json) (string, error) {
|
||||
var email string
|
||||
var err error
|
||||
func (p *AzureProvider) getGroupsFromProfileAPI(ctx context.Context, s *sessions.SessionState) ([]string, error) {
|
||||
if s.AccessToken == "" {
|
||||
return nil, fmt.Errorf("missing access token")
|
||||
}
|
||||
|
||||
email, err = json.Get("mail").String()
|
||||
groupsURL := getMicrosoftGraphGroupsURL(p.GraphGroupField).String()
|
||||
|
||||
// Need and extra header while talking with MS Graph. For more context see
|
||||
// https://docs.microsoft.com/en-us/graph/api/group-list-transitivememberof?view=graph-rest-1.0&tabs=http#request-headers
|
||||
extraHeader := makeAzureHeader(s.AccessToken)
|
||||
extraHeader.Add("ConsistencyLevel", "eventual")
|
||||
|
||||
var groups []string
|
||||
|
||||
for groupsURL != "" {
|
||||
jsonRequest, err := requests.New(groupsURL).
|
||||
WithContext(ctx).
|
||||
WithHeaders(extraHeader).
|
||||
Do().
|
||||
UnmarshalSimpleJSON()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal Microsoft Graph response: %v", err)
|
||||
|
||||
}
|
||||
groupsURL, err = jsonRequest.Get("@odata.nextLink").String()
|
||||
if err != nil {
|
||||
groupsURL = ""
|
||||
}
|
||||
groupsPage := getGroupsFromJSON(jsonRequest, p.GraphGroupField)
|
||||
groups = append(groups, groupsPage...)
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func getGroupsFromJSON(json *simplejson.Json, graphGroupField string) []string {
|
||||
groups := []string{}
|
||||
|
||||
for i := range json.Get("value").MustArray() {
|
||||
value := json.Get("value").GetIndex(i).Get(graphGroupField).MustString()
|
||||
groups = append(groups, value)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
func (p *AzureProvider) getEmailFromProfileAPI(ctx context.Context, accessToken string) (string, error) {
|
||||
if accessToken == "" {
|
||||
return "", fmt.Errorf("missing access token")
|
||||
}
|
||||
|
||||
json, err := requests.New(p.ProfileURL.String()).
|
||||
WithContext(ctx).
|
||||
WithHeaders(makeAzureHeader(accessToken)).
|
||||
Do().
|
||||
UnmarshalSimpleJSON()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
email, err := getEmailFromJSON(json)
|
||||
if email == "" {
|
||||
return "", fmt.Errorf("empty email address: %v", err)
|
||||
}
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func getEmailFromJSON(json *simplejson.Json) (string, error) {
|
||||
email, err := json.Get("mail").String()
|
||||
|
||||
if err != nil || email == "" {
|
||||
otherMails, otherMailsErr := json.Get("otherMails").Array()
|
||||
@ -335,24 +449,7 @@ func getEmailFromJSON(json *simplejson.Json) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return email, err
|
||||
}
|
||||
|
||||
func (p *AzureProvider) getEmailFromProfileAPI(ctx context.Context, accessToken string) (string, error) {
|
||||
if accessToken == "" {
|
||||
return "", errors.New("missing access token")
|
||||
}
|
||||
|
||||
json, err := requests.New(p.ProfileURL.String()).
|
||||
WithContext(ctx).
|
||||
WithHeaders(makeAzureHeader(accessToken)).
|
||||
Do().
|
||||
UnmarshalSimpleJSON()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return getEmailFromJSON(json)
|
||||
return email, nil
|
||||
}
|
||||
|
||||
// ValidateSession validates the AccessToken
|
||||
|
@ -55,6 +55,7 @@ func testAzureProvider(hostname string, opts options.AzureOptions) *AzureProvide
|
||||
ProtectedResource: &url.URL{},
|
||||
Scope: "",
|
||||
EmailClaim: "email",
|
||||
GroupsClaim: "groups",
|
||||
Verifier: internaloidc.NewVerifier(oidc.NewVerifier(
|
||||
"https://issuer.example.com",
|
||||
fakeAzureKeySetStub{},
|
||||
@ -133,14 +134,9 @@ func TestAzureSetTenant(t *testing.T) {
|
||||
p := testAzureProvider("", options.AzureOptions{Tenant: "example"})
|
||||
assert.Equal(t, "Azure", p.Data().ProviderName)
|
||||
assert.Equal(t, "example", p.Tenant)
|
||||
assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/authorize",
|
||||
p.Data().LoginURL.String())
|
||||
assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/token",
|
||||
p.Data().RedeemURL.String())
|
||||
assert.Equal(t, "https://graph.microsoft.com/v1.0/me",
|
||||
p.Data().ProfileURL.String())
|
||||
assert.Equal(t, "https://graph.microsoft.com",
|
||||
p.Data().ProtectedResource.String())
|
||||
assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/authorize", p.Data().LoginURL.String())
|
||||
assert.Equal(t, "https://login.microsoftonline.com/example/oauth2/token", p.Data().RedeemURL.String())
|
||||
assert.Equal(t, "https://graph.microsoft.com/v1.0/me", p.Data().ProfileURL.String())
|
||||
assert.Equal(t, "https://graph.microsoft.com/v1.0/me", p.Data().ValidateURL.String())
|
||||
assert.Equal(t, "openid", p.Data().Scope)
|
||||
}
|
||||
@ -151,10 +147,25 @@ func testAzureBackend(payload string, accessToken, refreshToken string) *httptes
|
||||
|
||||
func testAzureBackendWithError(payload string, accessToken, refreshToken string, injectError bool) *httptest.Server {
|
||||
path := "/v1.0/me"
|
||||
pathGroups := path + "/transitiveMemberOf/microsoft.graph.group"
|
||||
|
||||
return httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
if (r.URL.Path != path) && r.Method != http.MethodPost {
|
||||
if r.URL.Path == pathGroups && r.Method == http.MethodGet {
|
||||
w.Write([]byte(`{
|
||||
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups(displayName,id)",
|
||||
"value": [
|
||||
{
|
||||
"displayName": "aa",
|
||||
"id": "11111111-2222-3333-4444-555555555555"
|
||||
},
|
||||
{
|
||||
"displayName": "bb",
|
||||
"id": "555555555555-4444-3333-2222-11111111"
|
||||
}
|
||||
]
|
||||
}`))
|
||||
} else if (r.URL.Path != path) && r.Method != http.MethodPost {
|
||||
w.WriteHeader(404)
|
||||
} else if r.Method == http.MethodPost && r.Body != nil {
|
||||
if injectError {
|
||||
@ -183,7 +194,7 @@ func TestAzureProviderEnrichSession(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
Description: "should return email using mail property from Azure backend",
|
||||
PayloadFromAzureBackend: `{ "mail": "user@windows.net" }`,
|
||||
PayloadFromAzureBackend: `{ "mail": "user@windows.net", "groups": ["aa", "bb"] }`,
|
||||
ExpectedEmail: "user@windows.net",
|
||||
},
|
||||
{
|
||||
@ -199,17 +210,17 @@ func TestAzureProviderEnrichSession(t *testing.T) {
|
||||
{
|
||||
Description: "should return error when Azure backend doesn't return email information",
|
||||
PayloadFromAzureBackend: `{ "mail": null, "otherMails": [], "userPrincipalName": null }`,
|
||||
ExpectedError: fmt.Errorf("unable to get email address: %v", errors.New("type assertion to string failed")),
|
||||
ExpectedError: fmt.Errorf("unable to get email address from profile URL: %v", errors.New("empty email address: type assertion to string failed")),
|
||||
},
|
||||
{
|
||||
Description: "should return specific error when unable to get email",
|
||||
PayloadFromAzureBackend: `{ "mail": null, "otherMails": [], "userPrincipalName": "" }`,
|
||||
ExpectedError: errors.New("unable to get email address"),
|
||||
ExpectedError: errors.New("unable to get email address from profile URL: empty email address: <nil>"),
|
||||
},
|
||||
{
|
||||
Description: "should return error when otherMails from Azure backend is not a valid type",
|
||||
PayloadFromAzureBackend: `{ "mail": null, "otherMails": "", "userPrincipalName": null }`,
|
||||
ExpectedError: fmt.Errorf("unable to get email address: %v", errors.New("type assertion to string failed")),
|
||||
ExpectedError: fmt.Errorf("unable to get email address from profile URL: %v", errors.New("empty email address: type assertion to string failed")),
|
||||
},
|
||||
{
|
||||
Description: "should not query profile api when email is already set in session",
|
||||
@ -224,13 +235,13 @@ func TestAzureProviderEnrichSession(t *testing.T) {
|
||||
b *httptest.Server
|
||||
host string
|
||||
)
|
||||
if testCase.PayloadFromAzureBackend != "" {
|
||||
b = testAzureBackend(testCase.PayloadFromAzureBackend, authorizedAccessToken, "")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
host = bURL.Host
|
||||
}
|
||||
b = testAzureBackend(testCase.PayloadFromAzureBackend, authorizedAccessToken, "")
|
||||
defer b.Close()
|
||||
|
||||
bURL, _ := url.Parse(b.URL)
|
||||
host = bURL.Host
|
||||
|
||||
p := testAzureProvider(host, options.AzureOptions{})
|
||||
session := CreateAuthorizedSession()
|
||||
session.Email = testCase.Email
|
||||
@ -250,18 +261,21 @@ func TestAzureProviderRedeem(t *testing.T) {
|
||||
EmailFromAccessToken string
|
||||
IsIDTokenMalformed bool
|
||||
InjectRedeemURLError bool
|
||||
Groups []string
|
||||
}{
|
||||
{
|
||||
Name: "with id_token returned",
|
||||
EmailFromIDToken: "foo1@example.com",
|
||||
RefreshToken: "some_refresh_token",
|
||||
ExpiresOn: time.Now().Add(time.Hour),
|
||||
Groups: []string{"aa", "bb"},
|
||||
},
|
||||
{
|
||||
Name: "without id_token returned, fallback to access token",
|
||||
EmailFromAccessToken: "foo2@example.com",
|
||||
RefreshToken: "some_refresh_token",
|
||||
ExpiresOn: time.Now().Add(time.Hour),
|
||||
Groups: []string{"aa", "bb"},
|
||||
},
|
||||
{
|
||||
Name: "id_token malformed, fallback to access token",
|
||||
@ -269,6 +283,7 @@ func TestAzureProviderRedeem(t *testing.T) {
|
||||
RefreshToken: "some_refresh_token",
|
||||
ExpiresOn: time.Now().Add(time.Hour),
|
||||
IsIDTokenMalformed: true,
|
||||
Groups: []string{"aa", "bb"},
|
||||
},
|
||||
{
|
||||
Name: "both id_token and access tokens are valid, return email from id_token",
|
||||
@ -276,6 +291,7 @@ func TestAzureProviderRedeem(t *testing.T) {
|
||||
EmailFromAccessToken: "foo3@example.com",
|
||||
RefreshToken: "some_refresh_token",
|
||||
ExpiresOn: time.Now().Add(time.Hour),
|
||||
Groups: []string{"aa", "bb"},
|
||||
},
|
||||
{
|
||||
Name: "redeem URL failed, should return error",
|
||||
@ -284,6 +300,7 @@ func TestAzureProviderRedeem(t *testing.T) {
|
||||
RefreshToken: "some_refresh_token",
|
||||
ExpiresOn: time.Now().Add(time.Hour),
|
||||
InjectRedeemURLError: true,
|
||||
Groups: []string{"aa", "bb"},
|
||||
},
|
||||
}
|
||||
|
||||
@ -295,7 +312,9 @@ func TestAzureProviderRedeem(t *testing.T) {
|
||||
var err error
|
||||
token := idTokenClaims{
|
||||
StandardClaims: jwt.StandardClaims{Audience: "cd6d4fae-f6a6-4a34-8454-2c6b598e9532"},
|
||||
Email: testCase.EmailFromIDToken}
|
||||
Email: testCase.EmailFromIDToken,
|
||||
Groups: []string{"aa", "bb"},
|
||||
}
|
||||
idTokenString, err = newSignedTestIDToken(token)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@ -303,7 +322,9 @@ func TestAzureProviderRedeem(t *testing.T) {
|
||||
var err error
|
||||
token := idTokenClaims{
|
||||
StandardClaims: jwt.StandardClaims{Audience: "cd6d4fae-f6a6-4a34-8454-2c6b598e9532"},
|
||||
Email: testCase.EmailFromAccessToken}
|
||||
Email: testCase.EmailFromAccessToken,
|
||||
Groups: []string{"aa", "bb"},
|
||||
}
|
||||
accessTokenString, err = newSignedTestIDToken(token)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@ -335,6 +356,7 @@ func TestAzureProviderRedeem(t *testing.T) {
|
||||
assert.Equal(t, accessTokenString, s.AccessToken)
|
||||
assert.Equal(t, testCase.ExpiresOn.Unix(), s.ExpiresOn.Unix())
|
||||
assert.Equal(t, testCase.RefreshToken, s.RefreshToken)
|
||||
assert.Equal(t, testCase.Groups, s.Groups)
|
||||
if testCase.EmailFromIDToken != "" {
|
||||
assert.Equal(t, testCase.EmailFromIDToken, s.Email)
|
||||
} else {
|
||||
@ -345,13 +367,24 @@ func TestAzureProviderRedeem(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureProviderProtectedResourceConfigured(t *testing.T) {
|
||||
func TestAzureProviderProtectedResourceConfiguredOAuthV1(t *testing.T) {
|
||||
p := testAzureProvider("", options.AzureOptions{})
|
||||
p.ProtectedResource, _ = url.Parse("http://my.resource.test")
|
||||
result := p.GetLoginURL("https://my.test.app/oauth", "", "", url.Values{})
|
||||
assert.Contains(t, result, "resource="+url.QueryEscape("http://my.resource.test"))
|
||||
}
|
||||
|
||||
func TestAzureProviderProtectedResourceConfiguredOAuthV2(t *testing.T) {
|
||||
p := testAzureProvider("", options.AzureOptions{})
|
||||
testURL := "http://my.resource.test"
|
||||
p.ProtectedResource, _ = url.Parse(testURL)
|
||||
p.isV2Endpoint = true
|
||||
result, _ := url.Parse(p.GetLoginURL("https://my.test.app/oauth", "", "", url.Values{}))
|
||||
parsedQuery, _ := url.ParseQuery(result.RawQuery)
|
||||
assert.NotContains(t, parsedQuery["scope"], " "+testURL)
|
||||
assert.NotContains(t, result.RawQuery, "resource="+url.QueryEscape(testURL))
|
||||
}
|
||||
|
||||
func TestAzureProviderRefresh(t *testing.T) {
|
||||
email := "foo@example.com"
|
||||
subject := "foo"
|
||||
|
@ -267,14 +267,16 @@ func (p *ProviderData) buildSessionFromClaims(rawIDToken, accessToken string) (*
|
||||
// considered unverified.
|
||||
verifyEmail := (p.EmailClaim == options.OIDCEmailClaim) && !p.AllowUnverifiedEmail
|
||||
|
||||
var verified bool
|
||||
exists, err := extractor.GetClaimInto("email_verified", &verified)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if verifyEmail {
|
||||
var verified bool
|
||||
exists, err := extractor.GetClaimInto("email_verified", &verified)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if verifyEmail && exists && !verified {
|
||||
return nil, fmt.Errorf("email in id_token (%s) isn't verified", ss.Email)
|
||||
if exists && !verified {
|
||||
return nil, fmt.Errorf("email in id_token (%s) isn't verified", ss.Email)
|
||||
}
|
||||
}
|
||||
|
||||
return ss, nil
|
||||
|
Loading…
x
Reference in New Issue
Block a user