git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #1418 from ianroberts/start-url-parameters
7dc984eJoel Speed
CHANGELOG.md+1
@@ -8,6 +8,7 @@
8
8
9## Changes since v7.2.1
9## Changes since v7.2.1
10
10
11+- [#1418](https://github.com/oauth2-proxy/oauth2-proxy/pull/1418) Support for passing arbitrary query parameters through from `/oauth2/start` to the identity provider's login URL. Configuration settings control which parameters are passed by default and precisely which values can be overridden per-request (@ianroberts)
11- [#1559](https://github.com/oauth2-proxy/oauth2-proxy/pull/1559) Introduce ProviderVerifier to clean up OIDC discovery code (@JoelSpeed)
12- [#1559](https://github.com/oauth2-proxy/oauth2-proxy/pull/1559) Introduce ProviderVerifier to clean up OIDC discovery code (@JoelSpeed)
12- [#1561](https://github.com/oauth2-proxy/oauth2-proxy/pull/1561) Add ppc64le support (@mgiessing)
13- [#1561](https://github.com/oauth2-proxy/oauth2-proxy/pull/1561) Add ppc64le support (@mgiessing)
13- [#1563](https://github.com/oauth2-proxy/oauth2-proxy/pull/1563) Ensure claim extractor does not attempt profile call when URL is empty (@JoelSpeed)
14- [#1563](https://github.com/oauth2-proxy/oauth2-proxy/pull/1563) Ensure claim extractor does not attempt profile call when URL is empty (@JoelSpeed)
docs/docs/configuration/alpha_config.md+94−3
@@ -264,6 +264,85 @@ make up the header value
264| `jwtKeyFile` | _string_ | JWTKeyFile is a path to the private key file in PEM format used to sign the JWT |
264| `jwtKeyFile` | _string_ | JWTKeyFile is a path to the private key file in PEM format used to sign the JWT |
265| `pubjwkURL` | _string_ | PubJWKURL is the JWK pubkey access endpoint |
265| `pubjwkURL` | _string_ | PubJWKURL is the JWK pubkey access endpoint |
266
266
267+### LoginURLParameter
268+
269+(**Appears on:** [Provider](#provider))
270+
271+LoginURLParameter is the configuration for a single query parameter that
272+can be passed through from the `/oauth2/start` endpoint to the IdP login
273+URL. The "default" option specifies the default value or values (if any)
274+that will be passed to the IdP for this parameter, and "allow" is a list
275+of options for ways in which this parameter can be set or overridden via
276+the query string to `/oauth2/start`.
277+If _only_ a default is specified and no "allow" then the parameter is
278+effectively fixed - the default value will always be used and anything
279+passed to the start URL will be ignored. If _only_ "allow" is specified
280+but no default then the parameter will only be passed on to the IdP if
281+the caller provides it, and no value will be sent otherwise.
282+
283+Examples:
284+
285+A parameter whose value is fixed
286+
287+```
288+name: organization
289+default:
290+- myorg
291+```
292+
293+A parameter that is not passed by default, but may be set to one of a
294+fixed set of values
295+
296+```
297+name: prompt
298+allow:
299+- value: login
300+- value: consent
301+- value: select_account
302+```
303+
304+A parameter that is passed by default but may be overridden by one of
305+a fixed set of values
306+
307+```
308+name: prompt
309+default: ["login"]
310+allow:
311+- value: consent
312+- value: select_account
313+```
314+
315+A parameter that may be overridden, but only by values that match a
316+regular expression. For example to restrict `login_hint` to email
317+addresses in your organization's domain:
318+
319+```
320+name: login_hint
321+allow:
322+- pattern: '^[^@]*@example\.com$'
323+# this allows at most one "@" sign, and requires "example.com" domain.
324+```
325+
326+Note that the YAML rules around exactly which characters are allowed
327+and/or require escaping in different types of string literals are
328+convoluted. For regular expressions the single quoted form is simplest
329+as backslash is not considered to be an escape character. Alternatively
330+use the "chomped block" format `|-`:
331+
332+```
333+- pattern: |-
334+ ^[^@]*@example\.com$
335+```
336+
337+The hyphen is important, a `|` block would have a trailing newline
338+character.
339+
340+| Field | Type | Description |
341+| ----- | ---- | ----------- |
342+| `name` | _string_ | Name specifies the name of the query parameter. |
343+| `default` | _[]string_ | _(Optional)_ Default specifies a default value or values that will be<br/>passed to the IdP if not overridden. |
344+| `allow` | _[[]URLParameterRule](#urlparameterrule)_ | _(Optional)_ Allow specifies rules about how the default (if any) may be<br/>overridden via the query string to `/oauth2/start`. Only<br/>values that match one or more of the allow rules will be<br/>forwarded to the IdP. |
345+
267### OIDCOptions
346### OIDCOptions
268
347
269(**Appears on:** [Provider](#provider))
348(**Appears on:** [Provider](#provider))
@@ -309,15 +388,13 @@ Provider holds all configuration for a single provider
309| `name` | _string_ | Name is the providers display name<br/>if set, it will be shown to the users in the login page. |
388| `name` | _string_ | Name is the providers display name<br/>if set, it will be shown to the users in the login page. |
310| `caFiles` | _[]string_ | CAFiles is a list of paths to CA certificates that should be used when connecting to the provider.<br/>If not specified, the default Go trust sources are used instead |
389| `caFiles` | _[]string_ | CAFiles is a list of paths to CA certificates that should be used when connecting to the provider.<br/>If not specified, the default Go trust sources are used instead |
311| `loginURL` | _string_ | LoginURL is the authentication endpoint |
390| `loginURL` | _string_ | LoginURL is the authentication endpoint |
391+| `loginURLParameters` | _[[]LoginURLParameter](#loginurlparameter)_ | LoginURLParameters defines the parameters that can be passed from the start URL to the IdP login URL |
312| `redeemURL` | _string_ | RedeemURL is the token redemption endpoint |
392| `redeemURL` | _string_ | RedeemURL is the token redemption endpoint |
313| `profileURL` | _string_ | ProfileURL is the profile access endpoint |
393| `profileURL` | _string_ | ProfileURL is the profile access endpoint |
314| `resource` | _string_ | ProtectedResource is the resource that is protected (Azure AD and ADFS only) |
394| `resource` | _string_ | ProtectedResource is the resource that is protected (Azure AD and ADFS only) |
315| `validateURL` | _string_ | ValidateURL is the access token validation endpoint |
395| `validateURL` | _string_ | ValidateURL is the access token validation endpoint |
316| `scope` | _string_ | Scope is the OAuth scope specification |
396| `scope` | _string_ | Scope is the OAuth scope specification |
317−| `prompt` | _string_ | Prompt is OIDC prompt |
318−| `approvalPrompt` | _string_ | ApprovalPrompt is the OAuth approval_prompt<br/>default is set to 'force' |
319| `allowedGroups` | _[]string_ | AllowedGroups is a list of restrict logins to members of this group |
397| `allowedGroups` | _[]string_ | AllowedGroups is a list of restrict logins to members of this group |
320−| `acrValues` | _string_ | AcrValues is a string of acr values |
321
398
322### ProviderType
399### ProviderType
323#### (`string` alias)
400#### (`string` alias)
@@ -377,6 +454,20 @@ as well as an optional minimal TLS version that is acceptable.
377| `Cert` | _[SecretSource](#secretsource)_ | Cert is the TLS certificate data to use.<br/>Typically this will come from a file. |
454| `Cert` | _[SecretSource](#secretsource)_ | Cert is the TLS certificate data to use.<br/>Typically this will come from a file. |
378| `MinVersion` | _string_ | MinVersion is the minimal TLS version that is acceptable.<br/>E.g. Set to "TLS1.3" to select TLS version 1.3 |
455| `MinVersion` | _string_ | MinVersion is the minimal TLS version that is acceptable.<br/>E.g. Set to "TLS1.3" to select TLS version 1.3 |
379
456
457+### URLParameterRule
458+
459+(**Appears on:** [LoginURLParameter](#loginurlparameter))
460+
461+URLParameterRule represents a rule by which query parameters
462+passed to the `/oauth2/start` endpoint are checked to determine whether
463+they are valid overrides for the given parameter passed to the IdP's
464+login URL. Either Value or Pattern should be supplied, not both.
465+
466+| Field | Type | Description |
467+| ----- | ---- | ----------- |
468+| `value` | _string_ | A Value rule matches just this specific value |
469+| `pattern` | _string_ | A Pattern rule gives a regular expression that must be matched by<br/>some substring of the value. The expression is _not_ automatically<br/>anchored to the start and end of the value, if you _want_ to restrict<br/>the whole parameter value you must anchor it yourself with `^` and `$`. |
470+
380### Upstream
471### Upstream
381
472
382(**Appears on:** [UpstreamConfig](#upstreamconfig))
473(**Appears on:** [UpstreamConfig](#upstreamconfig))
main_test.go+7−2
@@ -68,7 +68,6 @@ providers:
68 ID: google=oauth2-proxy
68 ID: google=oauth2-proxy
69 clientSecret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
69 clientSecret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
70 clientID: oauth2-proxy
70 clientID: oauth2-proxy
71− approvalPrompt: force
72 azureConfig:
71 azureConfig:
73 tenant: common
72 tenant: common
74 oidcConfig:
73 oidcConfig:
@@ -78,6 +77,10 @@ providers:
78 insecureSkipNonce: true
77 insecureSkipNonce: true
79 audienceClaims: [aud]
78 audienceClaims: [aud]
80 extraAudiences: []
79 extraAudiences: []
80+ loginURLParameters:
81+ - name: approval_prompt
82+ default:
83+ - force
81`
84`
82
85
83 const testCoreConfig = `
86 const testCoreConfig = `
@@ -154,7 +157,9 @@ redirect_url="http://localhost:4180/oauth2/callback"
154 ExtraAudiences: []string{},
157 ExtraAudiences: []string{},
155 InsecureSkipNonce: true,
158 InsecureSkipNonce: true,
156 },
159 },
157− ApprovalPrompt: "force",
160+ LoginURLParameters: []options.LoginURLParameter{
161+ {Name: "approval_prompt", Default: []string{"force"}},
162+ },
158 },
163 },
159 }
164 }
160 return opts
165 return opts
oauthproxy.go+12−1
@@ -611,6 +611,7 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
611 if p.SkipProviderButton {
611 if p.SkipProviderButton {
612 p.OAuthStart(rw, req)
612 p.OAuthStart(rw, req)
613 } else {
613 } else {
614+ // TODO - should we pass on /oauth2/sign_in query params to /oauth2/start?
614 p.SignInPage(rw, req, http.StatusOK)
615 p.SignInPage(rw, req, http.StatusOK)
615 }
616 }
616 }
617 }
@@ -671,6 +672,12 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
671
672
672// OAuthStart starts the OAuth2 authentication flow
673// OAuthStart starts the OAuth2 authentication flow
673func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
674func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
675+ // start the flow permitting login URL query parameters to be overridden from the request URL
676+ p.doOAuthStart(rw, req, req.URL.Query())
677+}
678+
679+func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, overrides url.Values) {
680+ extraParams := p.provider.Data().LoginURLParams(overrides)
674 prepareNoCache(rw)
681 prepareNoCache(rw)
675
682
676 csrf, err := cookies.NewCSRF(p.CookieOptions)
683 csrf, err := cookies.NewCSRF(p.CookieOptions)
@@ -692,6 +699,7 @@ func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
692 callbackRedirect,
699 callbackRedirect,
693 encodeState(csrf.HashOAuthState(), appRedirect),
700 encodeState(csrf.HashOAuthState(), appRedirect),
694 csrf.HashOIDCNonce(),
701 csrf.HashOIDCNonce(),
702+ extraParams,
695 )
703 )
696
704
697 if _, err := csrf.SetCookie(rw, req); err != nil {
705 if _, err := csrf.SetCookie(rw, req); err != nil {
@@ -871,7 +879,10 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
871
879
872 logger.Printf("No valid authentication in request. Initiating login.")
880 logger.Printf("No valid authentication in request. Initiating login.")
873 if p.SkipProviderButton {
881 if p.SkipProviderButton {
874− p.OAuthStart(rw, req)
882+ // start OAuth flow, but only with the default login URL params - do not
883+ // consider this request's query params as potential overrides, since
884+ // the user did not explicitly start the login flow
885+ p.doOAuthStart(rw, req, nil)
875 } else {
886 } else {
876 p.SignInPage(rw, req, http.StatusForbidden)
887 p.SignInPage(rw, req, http.StatusForbidden)
877 }
888 }
pkg/apis/options/legacy_options.go+18−3
@@ -632,10 +632,7 @@ func (l *LegacyProvider) convert() (Providers, error) {
632 ProtectedResource: l.ProtectedResource,
632 ProtectedResource: l.ProtectedResource,
633 ValidateURL: l.ValidateURL,
633 ValidateURL: l.ValidateURL,
634 Scope: l.Scope,
634 Scope: l.Scope,
635− Prompt: l.Prompt,
636− ApprovalPrompt: l.ApprovalPrompt,
637 AllowedGroups: l.AllowedGroups,
635 AllowedGroups: l.AllowedGroups,
638− AcrValues: l.AcrValues,
639 }
636 }
640
637
641 // This part is out of the switch section for all providers that support OIDC
638 // This part is out of the switch section for all providers that support OIDC
@@ -708,6 +705,24 @@ func (l *LegacyProvider) convert() (Providers, error) {
708 provider.ID = l.ProviderType + "=" + l.ClientID
705 provider.ID = l.ProviderType + "=" + l.ClientID
709 }
706 }
710
707
708+ // handle AcrValues, Prompt and ApprovalPrompt
709+ var urlParams []LoginURLParameter
710+ if l.AcrValues != "" {
711+ urlParams = append(urlParams, LoginURLParameter{Name: "acr_values", Default: []string{l.AcrValues}})
712+ }
713+ switch {
714+ case l.Prompt != "":
715+ urlParams = append(urlParams, LoginURLParameter{Name: "prompt", Default: []string{l.Prompt}})
716+ case l.ApprovalPrompt != "":
717+ urlParams = append(urlParams, LoginURLParameter{Name: "approval_prompt", Default: []string{l.ApprovalPrompt}})
718+ default:
719+ // match legacy behaviour by default - if neither prompt nor approval_prompt
720+ // specified, use approval_prompt=force
721+ urlParams = append(urlParams, LoginURLParameter{Name: "approval_prompt", Default: []string{"force"}})
722+ }
723+
724+ provider.LoginURLParameters = urlParams
725+
711 providers = append(providers, provider)
726 providers = append(providers, provider)
712
727
713 return providers, nil
728 return providers, nil
pkg/apis/options/legacy_options_test.go+34−5
@@ -118,6 +118,9 @@ var _ = Describe("Legacy Options", func() {
118 opts.Providers[0].OIDCConfig.InsecureSkipNonce = true
118 opts.Providers[0].OIDCConfig.InsecureSkipNonce = true
119 opts.Providers[0].OIDCConfig.AudienceClaims = []string{"aud"}
119 opts.Providers[0].OIDCConfig.AudienceClaims = []string{"aud"}
120 opts.Providers[0].OIDCConfig.ExtraAudiences = []string{}
120 opts.Providers[0].OIDCConfig.ExtraAudiences = []string{}
121+ opts.Providers[0].LoginURLParameters = []LoginURLParameter{
122+ {Name: "approval_prompt", Default: []string{"force"}},
123+ }
121
124
122 converted, err := legacyOpts.ToOptions()
125 converted, err := legacyOpts.ToOptions()
123 Expect(err).ToNot(HaveOccurred())
126 Expect(err).ToNot(HaveOccurred())
@@ -891,21 +894,41 @@ var _ = Describe("Legacy Options", func() {
891 // Non defaults for these options
894 // Non defaults for these options
892 clientID := "abcd"
895 clientID := "abcd"
893
896
897+ defaultURLParams := []LoginURLParameter{
898+ {Name: "approval_prompt", Default: []string{"force"}},
899+ }
900+
894 defaultProvider := Provider{
901 defaultProvider := Provider{
902+ ID: "google=" + clientID,
903+ ClientID: clientID,
904+ Type: "google",
905+ LoginURLParameters: defaultURLParams,
906+ }
907+ defaultLegacyProvider := LegacyProvider{
908+ ClientID: clientID,
909+ ProviderType: "google",
910+ }
911+
912+ defaultProviderWithPrompt := Provider{
895 ID: "google=" + clientID,
913 ID: "google=" + clientID,
896 ClientID: clientID,
914 ClientID: clientID,
897 Type: "google",
915 Type: "google",
916+ LoginURLParameters: []LoginURLParameter{
917+ {Name: "prompt", Default: []string{"switch_user"}},
918+ },
898 }
919 }
899− defaultLegacyProvider := LegacyProvider{
920+ defaultLegacyProviderWithPrompt := LegacyProvider{
900 ClientID: clientID,
921 ClientID: clientID,
901 ProviderType: "google",
922 ProviderType: "google",
923+ Prompt: "switch_user",
902 }
924 }
903
925
904 displayNameProvider := Provider{
926 displayNameProvider := Provider{
905− ID: "displayName",
927+ ID: "displayName",
906− Name: "displayName",
928+ Name: "displayName",
907− ClientID: clientID,
929+ ClientID: clientID,
908− Type: "google",
930+ Type: "google",
931+ LoginURLParameters: defaultURLParams,
909 }
932 }
910
933
911 displayNameLegacyProvider := LegacyProvider{
934 displayNameLegacyProvider := LegacyProvider{
@@ -923,6 +946,7 @@ var _ = Describe("Legacy Options", func() {
923 ServiceAccountJSON: "test.json",
946 ServiceAccountJSON: "test.json",
924 Groups: []string{"1", "2"},
947 Groups: []string{"1", "2"},
925 },
948 },
949+ LoginURLParameters: defaultURLParams,
926 }
950 }
927
951
928 internalConfigLegacyProvider := LegacyProvider{
952 internalConfigLegacyProvider := LegacyProvider{
@@ -950,6 +974,11 @@ var _ = Describe("Legacy Options", func() {
950 expectedProviders: Providers{defaultProvider},
974 expectedProviders: Providers{defaultProvider},
951 errMsg: "",
975 errMsg: "",
952 }),
976 }),
977+ Entry("with prompt setting", &convertProvidersTableInput{
978+ legacyProvider: defaultLegacyProviderWithPrompt,
979+ expectedProviders: Providers{defaultProviderWithPrompt},
980+ errMsg: "",
981+ }),
953 Entry("with provider display name", &convertProvidersTableInput{
982 Entry("with provider display name", &convertProvidersTableInput{
954 legacyProvider: displayNameLegacyProvider,
983 legacyProvider: displayNameLegacyProvider,
955 expectedProviders: Providers{displayNameProvider},
984 expectedProviders: Providers{displayNameProvider},
pkg/apis/options/login_url_parameters.gonew file+101
@@ -0,0 +1,101 @@
1+package options
2+
3+// LoginURLParameter is the configuration for a single query parameter that
4+// can be passed through from the `/oauth2/start` endpoint to the IdP login
5+// URL. The "default" option specifies the default value or values (if any)
6+// that will be passed to the IdP for this parameter, and "allow" is a list
7+// of options for ways in which this parameter can be set or overridden via
8+// the query string to `/oauth2/start`.
9+// If _only_ a default is specified and no "allow" then the parameter is
10+// effectively fixed - the default value will always be used and anything
11+// passed to the start URL will be ignored. If _only_ "allow" is specified
12+// but no default then the parameter will only be passed on to the IdP if
13+// the caller provides it, and no value will be sent otherwise.
14+//
15+// Examples:
16+//
17+// A parameter whose value is fixed
18+//
19+// ```
20+// name: organization
21+// default:
22+// - myorg
23+// ```
24+//
25+// A parameter that is not passed by default, but may be set to one of a
26+// fixed set of values
27+//
28+// ```
29+// name: prompt
30+// allow:
31+// - value: login
32+// - value: consent
33+// - value: select_account
34+// ```
35+//
36+// A parameter that is passed by default but may be overridden by one of
37+// a fixed set of values
38+//
39+// ```
40+// name: prompt
41+// default: ["login"]
42+// allow:
43+// - value: consent
44+// - value: select_account
45+// ```
46+//
47+// A parameter that may be overridden, but only by values that match a
48+// regular expression. For example to restrict `login_hint` to email
49+// addresses in your organization's domain:
50+//
51+// ```
52+// name: login_hint
53+// allow:
54+// - pattern: '^[^@]*@example\.com$'
55+// # this allows at most one "@" sign, and requires "example.com" domain.
56+// ```
57+//
58+// Note that the YAML rules around exactly which characters are allowed
59+// and/or require escaping in different types of string literals are
60+// convoluted. For regular expressions the single quoted form is simplest
61+// as backslash is not considered to be an escape character. Alternatively
62+// use the "chomped block" format `|-`:
63+//
64+// ```
65+// - pattern: |-
66+// ^[^@]*@example\.com$
67+// ```
68+//
69+// The hyphen is important, a `|` block would have a trailing newline
70+// character.
71+type LoginURLParameter struct {
72+ // Name specifies the name of the query parameter.
73+ Name string `json:"name"`
74+
75+ // Default specifies a default value or values that will be
76+ // passed to the IdP if not overridden.
77+ //+optional
78+ Default []string `json:"default,omitempty"`
79+
80+ // Allow specifies rules about how the default (if any) may be
81+ // overridden via the query string to `/oauth2/start`. Only
82+ // values that match one or more of the allow rules will be
83+ // forwarded to the IdP.
84+ //+optional
85+ Allow []URLParameterRule `json:"allow,omitempty"`
86+}
87+
88+// URLParameterRule represents a rule by which query parameters
89+// passed to the `/oauth2/start` endpoint are checked to determine whether
90+// they are valid overrides for the given parameter passed to the IdP's
91+// login URL. Either Value or Pattern should be supplied, not both.
92+type URLParameterRule struct {
93+ // A Value rule matches just this specific value
94+ Value *string `json:"value,omitempty"`
95+
96+ // A Pattern rule gives a regular expression that must be matched by
97+ // some substring of the value. The expression is _not_ automatically
98+ // anchored to the start and end of the value, if you _want_ to restrict
99+ // the whole parameter value you must anchor it yourself with `^` and `$`.
100+ Pattern *string `json:"pattern,omitempty"`
101+}
pkg/apis/options/providers.go+3−11
@@ -62,6 +62,8 @@ type Provider struct {
62
62
63 // LoginURL is the authentication endpoint
63 // LoginURL is the authentication endpoint
64 LoginURL string `json:"loginURL,omitempty"`
64 LoginURL string `json:"loginURL,omitempty"`
65+ // LoginURLParameters defines the parameters that can be passed from the start URL to the IdP login URL
66+ LoginURLParameters []LoginURLParameter `json:"loginURLParameters,omitempty"`
65 // RedeemURL is the token redemption endpoint
67 // RedeemURL is the token redemption endpoint
66 RedeemURL string `json:"redeemURL,omitempty"`
68 RedeemURL string `json:"redeemURL,omitempty"`
67 // ProfileURL is the profile access endpoint
69 // ProfileURL is the profile access endpoint
@@ -72,16 +74,8 @@ type Provider struct {
72 ValidateURL string `json:"validateURL,omitempty"`
74 ValidateURL string `json:"validateURL,omitempty"`
73 // Scope is the OAuth scope specification
75 // Scope is the OAuth scope specification
74 Scope string `json:"scope,omitempty"`
76 Scope string `json:"scope,omitempty"`
75− // Prompt is OIDC prompt
76− Prompt string `json:"prompt,omitempty"`
77− // ApprovalPrompt is the OAuth approval_prompt
78− // default is set to 'force'
79− ApprovalPrompt string `json:"approvalPrompt,omitempty"`
80 // AllowedGroups is a list of restrict logins to members of this group
77 // AllowedGroups is a list of restrict logins to members of this group
81 AllowedGroups []string `json:"allowedGroups,omitempty"`
78 AllowedGroups []string `json:"allowedGroups,omitempty"`
82−
83− // AcrValues is a string of acr values
84− AcrValues string `json:"acrValues,omitempty"`
85}
79}
86
80
87// ProviderType is used to enumerate the different provider type options
81// ProviderType is used to enumerate the different provider type options
@@ -243,9 +237,7 @@ type LoginGovOptions struct {
243func providerDefaults() Providers {
237func providerDefaults() Providers {
244 providers := Providers{
238 providers := Providers{
245 {
239 {
246− Type: "google",
240+ Type: "google",
247− Prompt: "", // Change to "login" when ApprovalPrompt officially deprecated
248− ApprovalPrompt: "force",
249 AzureConfig: AzureOptions{
241 AzureConfig: AzureOptions{
250 Tenant: "common",
242 Tenant: "common",
251 },
243 },
providers/adfs.go+1−2
@@ -61,8 +61,7 @@ func NewADFSProvider(p *ProviderData, opts options.ADFSOptions) *ADFSProvider {
61
61
62// GetLoginURL Override to double encode the state parameter. If not query params are lost
62// GetLoginURL Override to double encode the state parameter. If not query params are lost
63// More info here: https://docs.microsoft.com/en-us/powerapps/maker/portals/configure/configure-saml2-settings
63// More info here: https://docs.microsoft.com/en-us/powerapps/maker/portals/configure/configure-saml2-settings
64−func (p *ADFSProvider) GetLoginURL(redirectURI, state, nonce string) string {
64+func (p *ADFSProvider) GetLoginURL(redirectURI, state, nonce string, extraParams url.Values) string {
65− extraParams := url.Values{}
66 if !p.SkipNonce {
65 if !p.SkipNonce {
67 extraParams.Add("nonce", nonce)
66 extraParams.Add("nonce", nonce)
68 }
67 }
providers/adfs_test.go+2−2
@@ -168,7 +168,7 @@ var _ = Describe("ADFS Provider Tests", func() {
168 Scope: "",
168 Scope: "",
169 }, options.ADFSOptions{SkipScope: true})
169 }, options.ADFSOptions{SkipScope: true})
170
170
171− result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "")
171+ result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "", url.Values{})
172 Expect(result).NotTo(ContainSubstring("scope="))
172 Expect(result).NotTo(ContainSubstring("scope="))
173 })
173 })
174 })
174 })
@@ -189,7 +189,7 @@ var _ = Describe("ADFS Provider Tests", func() {
189 }, options.ADFSOptions{})
189 }, options.ADFSOptions{})
190
190
191 Expect(p.Data().Scope).To(Equal(in.expectedScope))
191 Expect(p.Data().Scope).To(Equal(in.expectedScope))
192− result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "")
192+ result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "", url.Values{})
193 Expect(result).To(ContainSubstring("scope=" + url.QueryEscape(in.expectedScope)))
193 Expect(result).To(ContainSubstring("scope=" + url.QueryEscape(in.expectedScope)))
194 },
194 },
195 Entry("should add slash", scopeTableInput{
195 Entry("should add slash", scopeTableInput{
providers/azure.go+1−2
@@ -103,8 +103,7 @@ func overrideTenantURL(current, defaultURL *url.URL, tenant, path string) {
103 }
103 }
104}
104}
105
105
106−func (p *AzureProvider) GetLoginURL(redirectURI, state, _ string) string {
106+func (p *AzureProvider) GetLoginURL(redirectURI, state, _ string, extraParams url.Values) string {
107− extraParams := url.Values{}
108 if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
107 if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
109 extraParams.Add("resource", p.ProtectedResource.String())
108 extraParams.Add("resource", p.ProtectedResource.String())
110 }
109 }
providers/azure_test.go+1−1
@@ -348,7 +348,7 @@ func TestAzureProviderRedeem(t *testing.T) {
348func TestAzureProviderProtectedResourceConfigured(t *testing.T) {
348func TestAzureProviderProtectedResourceConfigured(t *testing.T) {
349 p := testAzureProvider("", options.AzureOptions{})
349 p := testAzureProvider("", options.AzureOptions{})
350 p.ProtectedResource, _ = url.Parse("http://my.resource.test")
350 p.ProtectedResource, _ = url.Parse("http://my.resource.test")
351− result := p.GetLoginURL("https://my.test.app/oauth", "", "")
351+ result := p.GetLoginURL("https://my.test.app/oauth", "", "", url.Values{})
352 assert.Contains(t, result, "resource="+url.QueryEscape("http://my.resource.test"))
352 assert.Contains(t, result, "resource="+url.QueryEscape("http://my.resource.test"))
353}
353}
354
354
providers/logingov.go+2−3
@@ -270,9 +270,8 @@ func (p *LoginGovProvider) Redeem(ctx context.Context, _, code string) (*session
270}
270}
271
271
272// GetLoginURL overrides GetLoginURL to add login.gov parameters
272// GetLoginURL overrides GetLoginURL to add login.gov parameters
273−func (p *LoginGovProvider) GetLoginURL(redirectURI, state, _ string) string {
273+func (p *LoginGovProvider) GetLoginURL(redirectURI, state, _ string, extraParams url.Values) string {
274− extraParams := url.Values{}
274+ if len(extraParams["acr_values"]) == 0 {
275− if p.AcrValues == "" {
276 acr := "http://idmanagement.gov/ns/assurance/loa/1"
275 acr := "http://idmanagement.gov/ns/assurance/loa/1"
277 extraParams.Add("acr_values", acr)
276 extraParams.Add("acr_values", acr)
278 }
277 }
providers/logingov_test.go+1−1
@@ -337,7 +337,7 @@ func TestLoginGovProviderBadNonce(t *testing.T) {
337
337
338func TestLoginGovProviderGetLoginURL(t *testing.T) {
338func TestLoginGovProviderGetLoginURL(t *testing.T) {
339 p, _, _ := newLoginGovProvider()
339 p, _, _ := newLoginGovProvider()
340− result := p.GetLoginURL("http://redirect/", "", "")
340+ result := p.GetLoginURL("http://redirect/", "", "", url.Values{})
341 assert.Contains(t, result, "acr_values="+url.QueryEscape("http://idmanagement.gov/ns/assurance/loa/1"))
341 assert.Contains(t, result, "acr_values="+url.QueryEscape("http://idmanagement.gov/ns/assurance/loa/1"))
342 assert.Contains(t, result, "nonce=fakenonce")
342 assert.Contains(t, result, "nonce=fakenonce")
343}
343}
providers/oidc.go+1−2
@@ -34,8 +34,7 @@ func NewOIDCProvider(p *ProviderData, opts options.OIDCOptions) *OIDCProvider {
34var _ Provider = (*OIDCProvider)(nil)
34var _ Provider = (*OIDCProvider)(nil)
35
35
36// GetLoginURL makes the LoginURL with optional nonce support
36// GetLoginURL makes the LoginURL with optional nonce support
37−func (p *OIDCProvider) GetLoginURL(redirectURI, state, nonce string) string {
37+func (p *OIDCProvider) GetLoginURL(redirectURI, state, nonce string, extraParams url.Values) string {
38− extraParams := url.Values{}
39 if !p.SkipNonce {
38 if !p.SkipNonce {
40 extraParams.Add("nonce", nonce)
39 extraParams.Add("nonce", nonce)
41 }
40 }
providers/oidc_test.go+2−2
@@ -96,11 +96,11 @@ func TestOIDCProviderGetLoginURL(t *testing.T) {
96 nonce := base64.RawURLEncoding.EncodeToString(n)
96 nonce := base64.RawURLEncoding.EncodeToString(n)
97
97
98 // SkipNonce defaults to true
98 // SkipNonce defaults to true
99− skipNonce := provider.GetLoginURL("http://redirect/", "", nonce)
99+ skipNonce := provider.GetLoginURL("http://redirect/", "", nonce, url.Values{})
100 assert.NotContains(t, skipNonce, "nonce")
100 assert.NotContains(t, skipNonce, "nonce")
101
101
102 provider.SkipNonce = false
102 provider.SkipNonce = false
103− withNonce := provider.GetLoginURL("http://redirect/", "", nonce)
103+ withNonce := provider.GetLoginURL("http://redirect/", "", nonce, url.Values{})
104 assert.Contains(t, withNonce, fmt.Sprintf("nonce=%s", nonce))
104 assert.Contains(t, withNonce, fmt.Sprintf("nonce=%s", nonce))
105}
105}
106
106
providers/provider_data.go+101−9
@@ -7,6 +7,7 @@ import (
7 "io/ioutil"
7 "io/ioutil"
8 "net/http"
8 "net/http"
9 "net/url"
9 "net/url"
10+ "regexp"
10 "strings"
11 "strings"
11
12
12 "github.com/coreos/go-oidc/v3/oidc"
13 "github.com/coreos/go-oidc/v3/oidc"
@@ -32,15 +33,10 @@ type ProviderData struct {
32 ProfileURL *url.URL
33 ProfileURL *url.URL
33 ProtectedResource *url.URL
34 ProtectedResource *url.URL
34 ValidateURL *url.URL
35 ValidateURL *url.URL
35− // Auth request params & related, see
36− //https://openid.net/specs/openid-connect-basic-1_0.html#rfc.section.2.1.1.1
37− AcrValues string
38− ApprovalPrompt string // NOTE: Renamed to "prompt" in OAuth2
39− ClientID string
36+ ClientID string
40− ClientSecret string
37+ ClientSecret string
41− ClientSecretFile string
38+ ClientSecretFile string
42− Scope string
43− Prompt string
39+ Scope string
44
40
45 // Common OIDC options for any OIDC-based providers to consume
41 // Common OIDC options for any OIDC-based providers to consume
46 AllowUnverifiedEmail bool
42 AllowUnverifiedEmail bool
@@ -54,6 +50,8 @@ type ProviderData struct {
54 AllowedGroups map[string]struct{}
50 AllowedGroups map[string]struct{}
55
51
56 getAuthorizationHeaderFunc func(string) http.Header
52 getAuthorizationHeaderFunc func(string) http.Header
53+ loginURLParameterDefaults url.Values
54+ loginURLParameterOverrides map[string]*regexp.Regexp
57}
55}
58
56
59// Data returns the ProviderData
57// Data returns the ProviderData
@@ -73,6 +71,100 @@ func (p *ProviderData) GetClientSecret() (clientSecret string, err error) {
73 return string(fileClientSecret), nil
71 return string(fileClientSecret), nil
74}
72}
75
73
74+// LoginURLParams returns the parameter values that should be passed to the IdP
75+// login URL. This is the default set of parameters configured for this provider,
76+// optionally overridden by the given overrides (typically from the URL of the
77+// /oauth2/start request) according to the configured rules for this provider.
78+func (p *ProviderData) LoginURLParams(overrides url.Values) url.Values {
79+ // the returned url.Values may be modified later in the request handling process
80+ // so shallow clone the default map
81+ params := url.Values{}
82+ for k, v := range p.loginURLParameterDefaults {
83+ params[k] = v
84+ }
85+ if len(overrides) > 0 {
86+ for param, re := range p.loginURLParameterOverrides {
87+ if reqValues, ok := overrides[param]; ok {
88+ actualValues := make([]string, 0, len(reqValues))
89+ for _, val := range reqValues {
90+ if re.MatchString(val) {
91+ actualValues = append(actualValues, val)
92+ }
93+ }
94+ if len(actualValues) > 0 {
95+ params.Del(param)
96+ params[param] = actualValues
97+ }
98+ }
99+ }
100+ }
101+ return params
102+}
103+
104+// Compile the given set of LoginURLParameter options into the internal defaults
105+// and regular expressions used to validate any overrides.
106+func (p *ProviderData) compileLoginParams(paramConfig []options.LoginURLParameter) []error {
107+ var errs []error
108+ p.loginURLParameterDefaults = url.Values{}
109+ p.loginURLParameterOverrides = make(map[string]*regexp.Regexp)
110+
111+ for _, param := range paramConfig {
112+ if p.seenParameter(param.Name) {
113+ errs = append(errs, fmt.Errorf("parameter %s provided more than once in loginURLParameters", param.Name))
114+ } else {
115+ // record default if parameter declares one
116+ if len(param.Default) > 0 {
117+ p.loginURLParameterDefaults[param.Name] = param.Default
118+ }
119+ // record allow rules if any
120+ if len(param.Allow) > 0 {
121+ errs = p.convertAllowRules(errs, param)
122+ }
123+ }
124+ }
125+ return errs
126+}
127+
128+// Converts the list of allow rules for the given parameter into a regexp
129+// and store it for use at runtime when validating overrides of that parameter.
130+func (p *ProviderData) convertAllowRules(errs []error, param options.LoginURLParameter) []error {
131+ var allowREs []string
132+ for idx, rule := range param.Allow {
133+ if (rule.Value == nil) == (rule.Pattern == nil) {
134+ errs = append(errs, fmt.Errorf("rule %d in LoginURLParameter %s must have exactly one of value or pattern", idx, param.Name))
135+ } else {
136+ allowREs = append(allowREs, regexpForRule(rule))
137+ }
138+ }
139+ if re, err := regexp.Compile(strings.Join(allowREs, "|")); err != nil {
140+ errs = append(errs, err)
141+ } else {
142+ p.loginURLParameterOverrides[param.Name] = re
143+ }
144+ return errs
145+}
146+
147+// Check whether we have already processed a configuration for the given parameter name
148+func (p *ProviderData) seenParameter(name string) bool {
149+ _, seenDefault := p.loginURLParameterDefaults[name]
150+ _, seenOverride := p.loginURLParameterOverrides[name]
151+ return seenDefault || seenOverride
152+}
153+
154+// Generate a validating regular expression pattern for a given URLParameterRule.
155+// If the rule is for a fixed value then returns a regexp that matches exactly
156+// that value, if the rule is itself a regexp just use that as-is.
157+func regexpForRule(rule options.URLParameterRule) string {
158+ if rule.Value != nil {
159+ // convert literal value into an equivalent regexp,
160+ // anchored at start and end
161+ return "^" + regexp.QuoteMeta(*rule.Value) + "$"
162+ }
163+ // just use the pattern as-is, but wrap in a non-capture group
164+ // to avoid any possibility of confusing the outer disjunction.
165+ return "(?:" + *rule.Pattern + ")"
166+}
167+
76// setAllowedGroups organizes a group list into the AllowedGroups map
168// setAllowedGroups organizes a group list into the AllowedGroups map
77// to be consumed by Authorize implementations
169// to be consumed by Authorize implementations
78func (p *ProviderData) setAllowedGroups(groups []string) {
170func (p *ProviderData) setAllowedGroups(groups []string) {
providers/provider_data_test.go+114
@@ -8,10 +8,14 @@ import (
8 "encoding/json"
8 "encoding/json"
9 "errors"
9 "errors"
10 "fmt"
10 "fmt"
11+ "net/url"
11 "strings"
12 "strings"
12 "testing"
13 "testing"
13 "time"
14 "time"
14
15
16+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
17+ "github.com/stretchr/testify/assert"
18+
15 "github.com/coreos/go-oidc/v3/oidc"
19 "github.com/coreos/go-oidc/v3/oidc"
16 "github.com/golang-jwt/jwt"
20 "github.com/golang-jwt/jwt"
17 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
21 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
@@ -498,3 +502,113 @@ func TestProviderData_checkNonce(t *testing.T) {
498 })
502 })
499 }
503 }
500}
504}
505+
506+func TestProviderData_loginURLParameters(t *testing.T) {
507+
508+ testCases := []struct {
509+ name string
510+ overrides url.Values
511+ has url.Values
512+ notHas []string
513+ }{
514+ {
515+ name: "no overrides",
516+ overrides: url.Values{},
517+ has: url.Values{
518+ "fixed": {"fixed-value"},
519+ "enum_with_default": {"default-value"},
520+ "free_with_default": {"default-value"},
521+ },
522+ notHas: []string{"enum_no_default", "free_no_default"},
523+ },
524+ {
525+ name: "attempt to override fixed value",
526+ overrides: url.Values{"fixed": {"another-value"}},
527+ has: url.Values{
528+ "fixed": {"fixed-value"},
529+ "enum_with_default": {"default-value"},
530+ "free_with_default": {"default-value"},
531+ },
532+ notHas: []string{"enum_no_default", "free_no_default"},
533+ },
534+ {
535+ name: "set one allowed and one forbidden enum",
536+ overrides: url.Values{
537+ "enum_no_default": {"allowed1", "not-allowed"},
538+ },
539+ has: url.Values{
540+ "fixed": {"fixed-value"},
541+ "enum_with_default": {"default-value"},
542+ "free_with_default": {"default-value"},
543+ "enum_no_default": {"allowed1"},
544+ },
545+ notHas: []string{"free_no_default"},
546+ },
547+ {
548+ name: "replace default value",
549+ overrides: url.Values{"free_with_default": {"something-else"}},
550+ has: url.Values{
551+ "fixed": {"fixed-value"},
552+ "enum_with_default": {"default-value"},
553+ "free_with_default": {"something-else"},
554+ },
555+ notHas: []string{"enum_no_default", "free_no_default"},
556+ },
557+ {
558+ name: "set free text value",
559+ overrides: url.Values{"free_no_default": {"some-value"}},
560+ has: url.Values{
561+ "fixed": {"fixed-value"},
562+ "enum_with_default": {"default-value"},
563+ "free_with_default": {"default-value"},
564+ "free_no_default": {"some-value"},
565+ },
566+ notHas: []string{"enum_no_default"},
567+ },
568+ {
569+ name: "attempt to set unapproved parameter",
570+ overrides: url.Values{"malicious_value": {"evil"}},
571+ has: url.Values{
572+ "fixed": {"fixed-value"},
573+ "enum_with_default": {"default-value"},
574+ "free_with_default": {"default-value"},
575+ },
576+ notHas: []string{"enum_no_default", "free_no_default"},
577+ },
578+ }
579+
580+ // fixed list of two allowed values
581+ allowed1 := "allowed1"
582+ allowed2 := "allowed2"
583+ allowEnum := []options.URLParameterRule{
584+ {Value: &allowed1},
585+ {Value: &allowed2},
586+ }
587+ // regex that will allow anything
588+ anything := "^.*$"
589+ allowAnything := []options.URLParameterRule{
590+ {Pattern: &anything},
591+ }
592+ for _, tc := range testCases {
593+ t.Run(tc.name, func(t *testing.T) {
594+ // set up LoginURLParameters for testing
595+ data := ProviderData{}
596+ data.compileLoginParams([]options.LoginURLParameter{
597+ {Name: "fixed", Default: []string{"fixed-value"}},
598+ {Name: "enum_with_default", Default: []string{"default-value"}, Allow: allowEnum},
599+ {Name: "enum_no_default", Allow: allowEnum},
600+ {Name: "free_with_default", Default: []string{"default-value"}, Allow: allowAnything},
601+ {Name: "free_no_default", Allow: allowAnything},
602+ })
603+
604+ redirectParams := data.LoginURLParams(tc.overrides)
605+ for _, k := range tc.notHas {
606+ assert.NotContains(t, redirectParams, k)
607+ }
608+ for k, vs := range tc.has {
609+ actualVals := redirectParams[k]
610+ assert.ElementsMatch(t, vs, actualVals)
611+ }
612+ })
613+ }
614+}
providers/provider_default.go+1−2
@@ -33,8 +33,7 @@ var (
33)
33)
34
34
35// GetLoginURL with typical oauth parameters
35// GetLoginURL with typical oauth parameters
36−func (p *ProviderData) GetLoginURL(redirectURI, state, _ string) string {
36+func (p *ProviderData) GetLoginURL(redirectURI, state, _ string, extraParams url.Values) string {
37− extraParams := url.Values{}
38 loginURL := makeLoginURL(p, redirectURI, state, extraParams)
37 loginURL := makeLoginURL(p, redirectURI, state, extraParams)
39 return loginURL.String()
38 return loginURL.String()
40}
39}
providers/provider_default_test.go−28
@@ -2,7 +2,6 @@ package providers
2
2
3import (
3import (
4 "context"
4 "context"
5− "net/url"
6 "testing"
5 "testing"
7 "time"
6 "time"
8
7
@@ -30,33 +29,6 @@ func TestRefresh(t *testing.T) {
30 assert.Equal(t, ErrNotImplemented, err)
29 assert.Equal(t, ErrNotImplemented, err)
31}
30}
32
31
33−func TestAcrValuesNotConfigured(t *testing.T) {
34− p := &ProviderData{
35− LoginURL: &url.URL{
36− Scheme: "http",
37− Host: "my.test.idp",
38− Path: "/oauth/authorize",
39− },
40− }
41−
42− result := p.GetLoginURL("https://my.test.app/oauth", "", "")
43− assert.NotContains(t, result, "acr_values")
44−}
45−
46−func TestAcrValuesConfigured(t *testing.T) {
47− p := &ProviderData{
48− LoginURL: &url.URL{
49− Scheme: "http",
50− Host: "my.test.idp",
51− Path: "/oauth/authorize",
52− },
53− AcrValues: "testValue",
54− }
55−
56− result := p.GetLoginURL("https://my.test.app/oauth", "", "")
57− assert.Contains(t, result, "acr_values=testValue")
58−}
59−
60func TestProviderDataEnrichSession(t *testing.T) {
32func TestProviderDataEnrichSession(t *testing.T) {
61 g := NewWithT(t)
33 g := NewWithT(t)
62 p := &ProviderData{}
34 p := &ProviderData{}
providers/providers.go+4−4
@@ -14,7 +14,7 @@ import (
14// Provider represents an upstream identity provider implementation
14// Provider represents an upstream identity provider implementation
15type Provider interface {
15type Provider interface {
16 Data() *ProviderData
16 Data() *ProviderData
17− GetLoginURL(redirectURI, finalRedirect string, nonce string) string
17+ GetLoginURL(redirectURI, finalRedirect string, nonce string, extraParams url.Values) string
18 Redeem(ctx context.Context, redirectURI, code string) (*sessions.SessionState, error)
18 Redeem(ctx context.Context, redirectURI, code string) (*sessions.SessionState, error)
19 // Deprecated: Migrate to EnrichSession
19 // Deprecated: Migrate to EnrichSession
20 GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error)
20 GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error)
@@ -70,9 +70,6 @@ func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData,
70 ClientID: providerConfig.ClientID,
70 ClientID: providerConfig.ClientID,
71 ClientSecret: providerConfig.ClientSecret,
71 ClientSecret: providerConfig.ClientSecret,
72 ClientSecretFile: providerConfig.ClientSecretFile,
72 ClientSecretFile: providerConfig.ClientSecretFile,
73− Prompt: providerConfig.Prompt,
74− ApprovalPrompt: providerConfig.ApprovalPrompt,
75− AcrValues: providerConfig.AcrValues,
76 }
73 }
77
74
78 needsVerifier, err := providerRequiresOIDCProviderVerifier(providerConfig.Type)
75 needsVerifier, err := providerRequiresOIDCProviderVerifier(providerConfig.Type)
@@ -122,6 +119,9 @@ func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData,
122 errs = append(errs, fmt.Errorf("could not parse %s URL: %v", name, err))
119 errs = append(errs, fmt.Errorf("could not parse %s URL: %v", name, err))
123 }
120 }
124 }
121 }
122+ // handle LoginURLParameters
123+ errs = append(errs, p.compileLoginParams(providerConfig.LoginURLParameters)...)
124+
125 if len(errs) > 0 {
125 if len(errs) > 0 {
126 return nil, k8serrors.NewAggregate(errs)
126 return nil, k8serrors.NewAggregate(errs)
127 }
127 }
providers/util.go−8
@@ -38,14 +38,6 @@ func makeLoginURL(p *ProviderData, redirectURI, state string, extraParams url.Va
38 a := *p.LoginURL
38 a := *p.LoginURL
39 params, _ := url.ParseQuery(a.RawQuery)
39 params, _ := url.ParseQuery(a.RawQuery)
40 params.Set("redirect_uri", redirectURI)
40 params.Set("redirect_uri", redirectURI)
41− if p.AcrValues != "" {
42− params.Add("acr_values", p.AcrValues)
43− }
44− if p.Prompt != "" {
45− params.Set("prompt", p.Prompt)
46− } else { // Legacy variant of the prompt param:
47− params.Set("approval_prompt", p.ApprovalPrompt)
48− }
49 params.Add("scope", p.Scope)
41 params.Add("scope", p.Scope)
50 params.Set("client_id", p.ClientID)
42 params.Set("client_id", p.ClientID)
51 params.Set("response_type", "code")
43 params.Set("response_type", "code")