Skip to content

Commit

Permalink
Merge pull request #644 from vkareh/rosa-cognito
Browse files Browse the repository at this point in the history
authentication: Allow client credential grants with basic auth
  • Loading branch information
vkareh authored May 26, 2022
2 parents 838dca9 + dd752eb commit 6359491
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 45 deletions.
99 changes: 55 additions & 44 deletions authentication/transport_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,38 +387,41 @@ func (b *TransportWrapperBuilder) Build(ctx context.Context) (result *TransportW
err = fmt.Errorf("claims of token %d are of type '%T'", i, claims)
return
}
claim, ok := claims["typ"]
claim, ok := claims["token_use"]
if !ok {
// When the token doesn't have the `typ` claim we will use the position to
// decide: first token should be the access token and second should be the
// refresh token. That is consistent with the signature of the method that
// returns the tokens.
switch i {
case 0:
b.logger.Debug(
ctx,
"First token doesn't have a 'typ' claim, will assume "+
"that it is an access token",
)
accessToken = &tokenInfo{
text: text,
object: object,
claim, ok = claims["typ"]
if !ok {
// When the token doesn't have the `typ` claim we will use the position to
// decide: first token should be the access token and second should be the
// refresh token. That is consistent with the signature of the method that
// returns the tokens.
switch i {
case 0:
b.logger.Debug(
ctx,
"First token doesn't have a 'typ' claim, will assume "+
"that it is an access token",
)
accessToken = &tokenInfo{
text: text,
object: object,
}
continue
case 1:
b.logger.Debug(
ctx,
"Second token doesn't have a 'typ' claim, will assume "+
"that it is a refresh token",
)
refreshToken = &tokenInfo{
text: text,
object: object,
}
continue
default:
err = fmt.Errorf("token %d doesn't contain the 'typ' claim", i)
return
}
continue
case 1:
b.logger.Debug(
ctx,
"Second token doesn't have a 'typ' claim, will assume "+
"that it is a refresh token",
)
refreshToken = &tokenInfo{
text: text,
object: object,
}
continue
default:
err = fmt.Errorf("token %d doesn't contain the 'typ' claim", i)
return
}
}
typ, ok := claim.(string)
Expand All @@ -427,7 +430,7 @@ func (b *TransportWrapperBuilder) Build(ctx context.Context) (result *TransportW
return
}
switch strings.ToLower(typ) {
case "bearer":
case "access", "bearer":
accessToken = &tokenInfo{
text: text,
object: object,
Expand Down Expand Up @@ -857,12 +860,17 @@ func (w *TransportWrapper) currentTokens() (access, refresh string) {
func (w *TransportWrapper) sendClientCredentialsForm(ctx context.Context, attempt int) (code int,
result *internal.TokenResponse, err error) {
form := url.Values{}
headers := map[string]string{}
w.logger.Debug(ctx, "Requesting new token using the client credentials grant")
form.Set(grantTypeField, clientCredentialsGrant)
form.Set(clientIDField, w.clientID)
form.Set(clientSecretField, w.clientSecret)
form.Set(scopeField, strings.Join(w.scopes, " "))
return w.sendForm(ctx, form, attempt)
// Encode client_id and client_secret to use as basic auth
// https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
auth := fmt.Sprintf("%s:%s", w.clientID, w.clientSecret)
hash := base64.StdEncoding.EncodeToString([]byte(auth))
headers["Authorization"] = fmt.Sprintf("Basic %s", hash)
return w.sendForm(ctx, form, headers, attempt)
}

func (w *TransportWrapper) sendPasswordForm(ctx context.Context, attempt int) (code int,
Expand All @@ -874,7 +882,7 @@ func (w *TransportWrapper) sendPasswordForm(ctx context.Context, attempt int) (c
form.Set(usernameField, w.user)
form.Set(passwordField, w.password)
form.Set(scopeField, strings.Join(w.scopes, " "))
return w.sendForm(ctx, form, attempt)
return w.sendForm(ctx, form, nil, attempt)
}

func (w *TransportWrapper) sendRefreshForm(ctx context.Context, attempt int) (code int,
Expand All @@ -884,15 +892,15 @@ func (w *TransportWrapper) sendRefreshForm(ctx context.Context, attempt int) (co
form.Set(grantTypeField, refreshTokenGrant)
form.Set(clientIDField, w.clientID)
form.Set(refreshTokenField, w.refreshToken.text)
code, result, err = w.sendForm(ctx, form, attempt)
code, result, err = w.sendForm(ctx, form, nil, attempt)
return
}

func (w *TransportWrapper) sendForm(ctx context.Context, form url.Values,
func (w *TransportWrapper) sendForm(ctx context.Context, form url.Values, headers map[string]string,
attempt int) (code int, result *internal.TokenResponse, err error) {
// Measure the time that it takes to send the request and receive the response:
start := time.Now()
code, result, err = w.sendFormTimed(ctx, form)
code, result, err = w.sendFormTimed(ctx, form, headers)
elapsed := time.Since(start)

// Update the metrics:
Expand All @@ -913,7 +921,7 @@ func (w *TransportWrapper) sendForm(ctx context.Context, form url.Values,
return
}

func (w *TransportWrapper) sendFormTimed(ctx context.Context, form url.Values) (code int,
func (w *TransportWrapper) sendFormTimed(ctx context.Context, form url.Values, headers map[string]string) (code int,
result *internal.TokenResponse, err error) {
// Create the HTTP request:
body := []byte(form.Encode())
Expand All @@ -925,6 +933,10 @@ func (w *TransportWrapper) sendFormTimed(ctx context.Context, form url.Values) (
}
header.Set("Content-Type", "application/x-www-form-urlencoded")
header.Set("Accept", "application/json")
// Add any additional headers:
for k, v := range headers {
header.Set(k, v)
}
if err != nil {
err = fmt.Errorf("can't create request: %w", err)
return
Expand Down Expand Up @@ -1012,15 +1024,15 @@ func (w *TransportWrapper) sendFormTimed(ctx context.Context, form url.Values) (
}
}

// The refresh token isn't mandatory for the password and client credentials grants:
// If a refresh token is not included in the response, we can safely assume that the old
// one is still valid and does not need to be discarded
// https://datatracker.ietf.org/doc/html/rfc6749#section-6
var refreshTokenText string
var refreshTokenObject *jwt.Token
var refreshToken *tokenInfo
if result.RefreshToken == nil {
grantType := form.Get(grantTypeField)
if grantType != passwordGrant && grantType != clientCredentialsGrant {
err = fmt.Errorf("no refresh token was received")
return
if w.refreshToken != nil && w.refreshToken.text != "" {
result.RefreshToken = &w.refreshToken.text
}
} else {
refreshTokenText = *result.RefreshToken
Expand Down Expand Up @@ -1104,7 +1116,6 @@ func parsePullSecretAccessToken(text string) error {
const (
grantTypeField = "grant_type"
clientIDField = "client_id"
clientSecretField = "client_secret"
usernameField = "username"
passwordField = "password"
refreshTokenField = "refresh_token"
Expand Down
53 changes: 52 additions & 1 deletion authentication/transport_wrapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,57 @@ var _ = Describe("Tokens", func() {
Expect(returnedAccess).To(Equal(validAccess))
Expect(returnedRefresh).To(Equal(validRefresh))
})

It("Uses client credentials grant with basic authentication and opaque refresh token", func() {
// Generate the tokens:
expiredAccess := MakeTokenString("Bearer", -5*time.Minute)
validAccess := MakeTokenString("Bearer", 5*time.Minute)
opaqueRefresh := "a.bb.ccc.dddd.eeeee"

// Configure the server so that it returns a expired access token for the
// first request, and then a valid access token for the second request. In
// both cases the client should be using the client credentials grant with
// basic authentication.
server.AppendHandlers(
CombineHandlers(
VerifyClientCredentialsGrant("myclient", "mysecret"),
RespondWithAccessToken(expiredAccess),
),
CombineHandlers(
VerifyClientCredentialsGrant("myclient", "mysecret"),
RespondWithAccessToken(validAccess),
),
)

// Create the wrapper:
wrapper, err := NewTransportWrapper().
Logger(logger).
TokenURL(server.URL()).
TrustedCA(ca).
Client("myclient", "mysecret").
Tokens(expiredAccess, opaqueRefresh).
Build(ctx)
Expect(err).ToNot(HaveOccurred())
defer func() {
err = wrapper.Close()
Expect(err).ToNot(HaveOccurred())
}()

// Force the initial token request. This will return an expired access token
// and a valid refresh token, that way when we get the tokens again the
// wrapper should send another request, but using the client credentials
// grant and ignoring the refresh token.
returnedAccess, returnedRefresh, err := wrapper.Tokens(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(returnedAccess).To(Equal(expiredAccess))
Expect(returnedRefresh).To(Equal(opaqueRefresh))

// Force another request:
returnedAccess, returnedRefresh, err = wrapper.Tokens(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(returnedAccess).To(Equal(validAccess))
Expect(returnedRefresh).To(Equal(opaqueRefresh))
})
})

Describe("Retry for getting access token", func() {
Expand Down Expand Up @@ -1445,9 +1496,9 @@ func VerifyClientCredentialsGrant(id, secret string) http.HandlerFunc {
return CombineHandlers(
VerifyRequest(http.MethodPost, "/"),
VerifyContentType("application/x-www-form-urlencoded"),
VerifyBasicAuth(id, secret),
VerifyFormKV("grant_type", "client_credentials"),
VerifyFormKV("client_id", id),
VerifyFormKV("client_secret", secret),
)
}

Expand Down

0 comments on commit 6359491

Please sign in to comment.