This is an automated email from the ASF dual-hosted git repository.
mmerli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-client-go.git
The following commit(s) were added to refs/heads/master by this push:
new bfbb2a2 Improve support for Azure AD OAuth 2.0 (#630)
bfbb2a2 is described below
commit bfbb2a2eea0beef59004876a573a8b4a44a74a76
Author: Eron Wright <[email protected]>
AuthorDate: Mon Oct 4 22:29:36 2021 -0700
Improve support for Azure AD OAuth 2.0 (#630)
- use well-known claims for user name
- support content types with encodings
- support scope parameter
-
Co-authored-by: Matteo Merli <[email protected]>
---
oauth2/auth.go | 13 +++++++++++++
oauth2/authorization_tokenretriever.go | 20 ++++++++++++++++++--
oauth2/client_credentials_flow.go | 3 +++
oauth2/client_credentials_flow_test.go | 4 ++++
oauth2/device_code_flow.go | 2 ++
oauth2/device_code_provider.go | 8 ++++++--
oauth2/go.mod | 1 +
oauth2/go.sum | 12 ++----------
pulsar/internal/auth/oauth2.go | 4 +++-
9 files changed, 52 insertions(+), 15 deletions(-)
diff --git a/oauth2/auth.go b/oauth2/auth.go
index dc09e11..0a3c73a 100644
--- a/oauth2/auth.go
+++ b/oauth2/auth.go
@@ -28,6 +28,8 @@ import (
const (
ClaimNameUserName = "https://pulsar.apache.org/username"
+ ClaimNameName = "name"
+ ClaimNameSubject = "sub"
)
// Flow abstracts an OAuth 2.0 authentication and authorization flow
@@ -74,6 +76,10 @@ type AuthorizationGrant struct {
// Token contains an access token in the client credentials grant type,
// and a refresh token in the device authorization grant type
Token *oauth2.Token `json:"token,omitempty"`
+
+ // Scopes contains the scopes associated with the grant, or the scopes
+ // to request in the client credentials grant type
+ Scopes []string `json:"scopes,omitempty"`
}
// TokenResult holds token information
@@ -101,6 +107,7 @@ func convertToOAuth2Token(token *TokenResult, clock
clock.Clock) oauth2.Token {
}
// ExtractUserName extracts the username claim from an authorization grant
+// conforms to draft-ietf-oauth-access-token-jwt
func ExtractUserName(token oauth2.Token) (string, error) {
p := jwt.Parser{}
claims := jwt.MapClaims{}
@@ -109,6 +116,12 @@ func ExtractUserName(token oauth2.Token) (string, error) {
}
username, ok := claims[ClaimNameUserName]
if !ok {
+ username, ok = claims[ClaimNameName]
+ }
+ if !ok {
+ username, ok = claims[ClaimNameSubject]
+ }
+ if !ok {
return "", fmt.Errorf("access token doesn't contain a username
claim")
}
switch v := username.(type) {
diff --git a/oauth2/authorization_tokenretriever.go
b/oauth2/authorization_tokenretriever.go
index 93c1bfe..2aecbfa 100644
--- a/oauth2/authorization_tokenretriever.go
+++ b/oauth2/authorization_tokenretriever.go
@@ -22,6 +22,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "mime"
"net/http"
"net/url"
"strconv"
@@ -71,6 +72,7 @@ type ClientCredentialsExchangeRequest struct {
ClientID string
ClientSecret string
Audience string
+ Scopes []string
}
// DeviceCodeExchangeRequest is used to request the exchange of
@@ -195,7 +197,13 @@ func (ce *TokenRetriever) newClientCredentialsRequest(req
ClientCredentialsExcha
uv.Set("grant_type", "client_credentials")
uv.Set("client_id", req.ClientID)
uv.Set("client_secret", req.ClientSecret)
- uv.Set("audience", req.Audience)
+ if len(req.Scopes) > 0 {
+ uv.Set("scope", strings.Join(req.Scopes, " "))
+ }
+ if req.Audience != "" {
+ // Audience is an Auth0 extension; other providers use scopes
to similar effect.
+ uv.Set("audience", req.Audience)
+ }
euv := uv.Encode()
@@ -237,7 +245,15 @@ func (ce *TokenRetriever) handleAuthTokensResponse(resp
*http.Response) (*TokenR
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
- if resp.Header.Get("Content-Type") == "application/json" {
+ cth := resp.Header.Get("Content-Type")
+ if cth == "" {
+ cth = "application/json"
+ }
+ ct, _, err := mime.ParseMediaType(cth)
+ if err != nil {
+ return nil, fmt.Errorf("unprocessable content type: %s:
%w", cth, err)
+ }
+ if ct == "application/json" {
er := TokenErrorResponse{}
err := json.NewDecoder(resp.Body).Decode(&er)
if err != nil {
diff --git a/oauth2/client_credentials_flow.go
b/oauth2/client_credentials_flow.go
index 808b09b..2252144 100644
--- a/oauth2/client_credentials_flow.go
+++ b/oauth2/client_credentials_flow.go
@@ -100,6 +100,7 @@ func (c *ClientCredentialsFlow) Authorize(audience string)
(*AuthorizationGrant,
ClientID: c.keyfile.ClientID,
ClientCredentials: c.keyfile,
TokenEndpoint: c.oidcWellKnownEndpoints.TokenEndpoint,
+ Scopes: c.options.AdditionalScopes,
}
// test the credentials and obtain an initial access token
@@ -139,6 +140,7 @@ func (g *ClientCredentialsGrantRefresher) Refresh(grant
*AuthorizationGrant) (*A
Audience: grant.Audience,
ClientID: grant.ClientCredentials.ClientID,
ClientSecret: grant.ClientCredentials.ClientSecret,
+ Scopes: grant.Scopes,
}
tr, err := g.exchanger.ExchangeClientCredentials(exchangeRequest)
if err != nil {
@@ -153,6 +155,7 @@ func (g *ClientCredentialsGrantRefresher) Refresh(grant
*AuthorizationGrant) (*A
ClientCredentials: grant.ClientCredentials,
TokenEndpoint: grant.TokenEndpoint,
Token: &token,
+ Scopes: grant.Scopes,
}
return grant, nil
}
diff --git a/oauth2/client_credentials_flow_test.go
b/oauth2/client_credentials_flow_test.go
index 41c2cbb..987ae73 100644
--- a/oauth2/client_credentials_flow_test.go
+++ b/oauth2/client_credentials_flow_test.go
@@ -147,6 +147,7 @@ var _ = Describe("ClientCredentialsGrantRefresher", func() {
ClientCredentials: &clientCredentials,
TokenEndpoint: oidcEndpoints.TokenEndpoint,
Token: nil,
+ Scopes: []string{"profile"},
}
_, err := refresher.Refresh(og)
Expect(err).ToNot(HaveOccurred())
@@ -155,6 +156,7 @@ var _ = Describe("ClientCredentialsGrantRefresher", func() {
ClientID: clientCredentials.ClientID,
ClientSecret: clientCredentials.ClientSecret,
Audience: og.Audience,
+ Scopes: og.Scopes,
}))
})
@@ -169,6 +171,7 @@ var _ = Describe("ClientCredentialsGrantRefresher", func() {
ClientCredentials: &clientCredentials,
TokenEndpoint: oidcEndpoints.TokenEndpoint,
Token: nil,
+ Scopes: []string{"profile"},
}
ng, err := refresher.Refresh(og)
Expect(err).ToNot(HaveOccurred())
@@ -178,6 +181,7 @@ var _ = Describe("ClientCredentialsGrantRefresher", func() {
Expect(ng.TokenEndpoint).To(Equal(oidcEndpoints.TokenEndpoint))
expected :=
convertToOAuth2Token(mockTokenExchanger.ReturnsTokens, mockClock)
Expect(*ng.Token).To(Equal(expected))
+ Expect(ng.Scopes).To(Equal([]string{"profile"}))
})
})
})
diff --git a/oauth2/device_code_flow.go b/oauth2/device_code_flow.go
index 486fdfa..d46148a 100644
--- a/oauth2/device_code_flow.go
+++ b/oauth2/device_code_flow.go
@@ -146,6 +146,7 @@ func (p *DeviceCodeFlow) Authorize(audience string)
(*AuthorizationGrant, error)
ClientID: p.options.ClientID,
TokenEndpoint: p.oidcWellKnownEndpoints.TokenEndpoint,
Token: &token,
+ Scopes: additionalScopes,
}
return grant, nil
}
@@ -198,6 +199,7 @@ func (g *DeviceAuthorizationGrantRefresher) Refresh(grant
*AuthorizationGrant) (
ClientID: grant.ClientID,
Token: &token,
TokenEndpoint: grant.TokenEndpoint,
+ Scopes: grant.Scopes,
}
return grant, nil
}
diff --git a/oauth2/device_code_provider.go b/oauth2/device_code_provider.go
index 23b226b..77e1eb9 100644
--- a/oauth2/device_code_provider.go
+++ b/oauth2/device_code_provider.go
@@ -98,8 +98,12 @@ func (cp *LocalDeviceCodeProvider) newDeviceCodeRequest(
req *DeviceCodeRequest) (*http.Request, error) {
uv := url.Values{}
uv.Set("client_id", req.ClientID)
- uv.Set("scope", strings.Join(req.Scopes, " "))
- uv.Set("audience", req.Audience)
+ if len(req.Scopes) > 0 {
+ uv.Set("scope", strings.Join(req.Scopes, " "))
+ }
+ if req.Audience != "" {
+ uv.Set("audience", req.Audience)
+ }
euv := uv.Encode()
request, err := http.NewRequest("POST",
diff --git a/oauth2/go.mod b/oauth2/go.mod
index 091477d..89d5015 100644
--- a/oauth2/go.mod
+++ b/oauth2/go.mod
@@ -4,6 +4,7 @@ go 1.13
require (
github.com/99designs/keyring v1.1.6
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/form3tech-oss/jwt-go v3.2.3+incompatible
github.com/onsi/ginkgo v1.14.0
github.com/onsi/gomega v1.10.1
diff --git a/oauth2/go.sum b/oauth2/go.sum
index a0c6f9d..48ffe44 100644
--- a/oauth2/go.sum
+++ b/oauth2/go.sum
@@ -1,6 +1,4 @@
cloud.google.com/go v0.34.0/go.mod
h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-github.com/99designs/keyring v1.1.5
h1:wLv7QyzYpFIyMSwOADq1CLTF9KbjbBfcnfmOGJ64aO4=
-github.com/99designs/keyring v1.1.5/go.mod
h1:7hsVvt2qXgtadGevGJ4ujg+u8m6SpJ5TpHqTozIPqf0=
github.com/99designs/keyring v1.1.6
h1:kVDC2uCgVwecxCk+9zoCt2uEL6dt+dfVzMvGgnVcIuM=
github.com/99designs/keyring v1.1.6/go.mod
h1:16e0ds7LGQQcT59QqkTg72Hh5ShM51Byv5PEmW6uoRU=
github.com/danieljoos/wincred v1.0.2
h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU=
@@ -8,9 +6,8 @@ github.com/danieljoos/wincred v1.0.2/go.mod
h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3E
github.com/davecgh/go-spew v1.1.0/go.mod
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1
h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod
h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
-github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a
h1:mq+R6XEM6lJX5VlLyZIrUSP8tSuJp82xTK89hvBwJbU=
-github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod
h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible
h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod
h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b
h1:HBah4D48ypg3J7Np4N+HY/ZR76fx3HEUGxDU6Uk39oQ=
github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b/go.mod
h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible
h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=
@@ -18,7 +15,6 @@ github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod
h1:pbq4aXjuKjdthFRnoD
github.com/fsnotify/fsnotify v1.4.7/go.mod
h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9
h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod
h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/go-logr/logr v0.1.0/go.mod
h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2
h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod
h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/golang/protobuf v1.2.0/go.mod
h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -60,7 +56,6 @@ github.com/pkg/errors v0.9.1
h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/spf13/afero v1.2.2/go.mod
h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/stretchr/objx v0.1.0/go.mod
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod
h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
@@ -113,6 +108,3 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod
h1:dt/ZhP58zS4L8KSrWD
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
-k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19
h1:7Nu2dTj82c6IaWvL7hImJzcXoTPz1MsSCH7r+0m6rfo=
-k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19/go.mod
h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
diff --git a/pulsar/internal/auth/oauth2.go b/pulsar/internal/auth/oauth2.go
index ec65ba8..924f22f 100644
--- a/pulsar/internal/auth/oauth2.go
+++ b/pulsar/internal/auth/oauth2.go
@@ -21,6 +21,7 @@ import (
"crypto/tls"
"fmt"
"net/http"
+ "strings"
xoauth2 "golang.org/x/oauth2"
@@ -35,6 +36,7 @@ const (
ConfigParamTypeClientCredentials = "client_credentials"
ConfigParamIssuerURL = "issuerUrl"
ConfigParamAudience = "audience"
+ ConfigParamScope = "scope"
ConfigParamKeyFile = "privateKey"
ConfigParamClientID = "clientId"
)
@@ -62,7 +64,7 @@ func NewAuthenticationOAuth2WithParams(params
map[string]string) (Provider, erro
case ConfigParamTypeClientCredentials:
flow, err :=
oauth2.NewDefaultClientCredentialsFlow(oauth2.ClientCredentialsFlowOptions{
KeyFile: params[ConfigParamKeyFile],
- AdditionalScopes: nil,
+ AdditionalScopes:
strings.Split(params[ConfigParamScope], ""),
})
if err != nil {
return nil, err