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

Reply via email to