Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

[![CircleCI](https://circleci.com/gh/palantir/go-oauth2-client/tree/develop.svg?style=svg)](https://circleci.com/gh/palantir/go-oauth2-client/tree/develop) [![](https://godoc.org/github.com/palantir/go-oauth2-client?status.svg)](http://godoc.org/github.com/palantir/go-oauth2-client)

A golang client for requesting client credentials from an OAuth2 server. See the [OAuth2 client_credentials specification](https://www.oauth.com/oauth2-servers/access-tokens/client-credentials/#parameters) for details.
A Golang client for requesting an access token from an OAuth2 server.

Support the following auth flows:
1. Client Credentials Flow. See the [OAuth2 `client_credentials` specification](https://www.oauth.com/oauth2-servers/access-tokens/client-credentials/) for details.
2. Authorization Code Flow. See the [OAuth2 `authorization_code` specification](https://www.oauth.com/oauth2-servers/access-tokens/authorization-code-request/) for details.

## License
This project is made available under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0).
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-269.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: feature
feature:
description: Implement support for `authorization_code` OAuth flow
links:
- https://github.com/palantir/go-oauth2-client/pull/269
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ module github.com/palantir/go-oauth2-client/v2
go 1.20

require (
github.com/Masterminds/goutils v1.1.1
github.com/palantir/conjure-go-runtime/v2 v2.61.0
github.com/palantir/pkg/retry v1.2.0
github.com/palantir/witchcraft-go-error v1.27.0
github.com/palantir/witchcraft-go-logging v1.44.0
github.com/palantir/witchcraft-go-params v1.25.0
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/stretchr/testify v1.8.4
)

Expand All @@ -27,6 +29,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.13.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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=
Expand Down Expand Up @@ -47,6 +49,8 @@ github.com/palantir/witchcraft-go-params v1.25.0 h1:9D1LrhNKIuOTWCh1nhO4DFlHYQWc
github.com/palantir/witchcraft-go-params v1.25.0/go.mod h1:MlO5fxylWlbFR/A1vESeFy13etiIhgKaIqoL9nXTK6I=
github.com/palantir/witchcraft-go-tracing v1.27.0 h1:mGe0PJs2Kx5vCVPx5pxgz5pCzfcngpOaceCHqIfHZf0=
github.com/palantir/witchcraft-go-tracing v1.27.0/go.mod h1:19VqVtHmMDAbU7Zy57Pxu10LHjjLxdiqSCzfCCZJdBg=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
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=
Expand All @@ -60,7 +64,9 @@ go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
Expand Down
7 changes: 6 additions & 1 deletion oauth/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import (
"context"
)

// ClientCredentialClient returns a client_credentials token
// ClientCredentialClient returns a token using Client Credentials flow
type ClientCredentialClient interface {
CreateClientCredentialToken(ctx context.Context, clientID, clientSecret string) (string, error)
}

// AuthorizationCodeClient returns a token using Authorization Code flow
type AuthorizationCodeClient interface {
CreateAuthorizationCodeToken(ctx context.Context, req AuthorizationCodeTokenRequest) (string, error)
}
79 changes: 79 additions & 0 deletions oauth/authorization_code_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) 2023 Palantir Technologies. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package oauth

import (
"context"
"net/http"
"net/url"

"github.com/palantir/conjure-go-runtime/v2/conjure-go-client/httpclient"
"github.com/palantir/conjure-go-runtime/v2/conjure-go-contract/codecs"
werror "github.com/palantir/witchcraft-go-error"
)

const (
authorizationCodeGrantType = "authorization_code"
)

type authorizationCodeClient struct {
client httpclient.Client
}

// NewAuthorizationCodeClient returns an AuthorizationCodeClient configured using the provided client.
func NewAuthorizationCodeClient(client httpclient.Client) AuthorizationCodeClient {
return &authorizationCodeClient{
client: client,
}
}

// AuthorizationCodeTokenRequest contains parameters in the request to get token in Authorization Code flow
type AuthorizationCodeTokenRequest struct {
ClientID string
Code string
CodeVerifier string
RedirectURI string
}

// URLValues returns url.Values representation of AuthorizationCodeTokenRequest
func (r AuthorizationCodeTokenRequest) URLValues() url.Values {
values := url.Values{
"grant_type": []string{authorizationCodeGrantType},
"client_id": []string{r.ClientID},
"code": []string{r.Code},
"code_verifier": []string{r.CodeVerifier},
}

if r.RedirectURI != "" {
values.Set("redirect_uri", r.RedirectURI)
}
return values
}

func (c *authorizationCodeClient) CreateAuthorizationCodeToken(ctx context.Context, req AuthorizationCodeTokenRequest) (string, error) {
var oauth2Resp oauth2Response
_, err := c.client.Do(ctx,
httpclient.WithRPCMethodName("CreateAuthorizationCodeToken"),
httpclient.WithRequestMethod(http.MethodPost),
httpclient.WithPath(oauthTokenEndpoint),
httpclient.WithRequestBody(req.URLValues(), codecs.FormURLEncoded),
httpclient.WithJSONResponse(&oauth2Resp),
httpclient.WithRequestErrorDecoder(errorDecoder{ctx}),
)
if err != nil {
return "", werror.WrapWithContextParams(ctx, err, "failed to make create authorization code token request")
}
return oauth2Resp.AccessToken, nil
}
145 changes: 145 additions & 0 deletions oauth/authorization_code_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (c) 2023 Palantir Technologies. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package oauth

import (
"context"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"path"

"github.com/Masterminds/goutils"
werror "github.com/palantir/witchcraft-go-error"
"github.com/pkg/browser"
)

const (
authorizeApplicationPath = "oauth2/authorize"
)

var (
// DefaultCallbackURL is the default callback URL used by the AuthorizationCodeHandler
DefaultCallbackURL = url.URL{
Scheme: "http",
Host: "localhost:8401",
Path: "/callback/oauth2",
}
)

// AuthorizationCodeHandler handles the callback part of Authorization Code flow
type AuthorizationCodeHandler interface {
PromptAndWaitForCode(ctx context.Context) (*AuthorizationCode, error)
}

// AuthorizationCode is a response returned from a callback in Authorization Code flow
type AuthorizationCode struct {
Code string
CodeVerifier string
ClientID string
}

type authorizationCodeHandler struct {
clientID string
loginBaseURL string
}

// NewAuthorizationCodeHandler returns a new Authorization Code flow handler with a localhost callback listener
// Expects loginBaseURL to point to a base URL of the OAuth login provider
func NewAuthorizationCodeHandler(clientID string, loginBaseURL string) AuthorizationCodeHandler {
return &authorizationCodeHandler{
clientID: clientID,
loginBaseURL: loginBaseURL,
}
}

// PromptAndWaitForCode opens a login URL in the browser, starts a local webserver listening on port 8401 for the OAuth callback,
// and returns the obtained authorization code once it is received by the callback
func (h *authorizationCodeHandler) PromptAndWaitForCode(ctx context.Context) (*AuthorizationCode, error) {
l, err := net.Listen("tcp", DefaultCallbackURL.Host)
if err != nil {
return nil, werror.WrapWithContextParams(ctx, err, "failed to create callback handling server")
}

resultsCh := make(chan string)
errorsCh := make(chan error)
serveMux := http.NewServeMux()
serveMux.HandleFunc(DefaultCallbackURL.Path, newRedirectHandler(resultsCh, errorsCh))

s := &http.Server{Handler: serveMux}
go func() {
errorsCh <- s.Serve(l)
}()

defer func() {
_ = s.Close()
}()

codeVerifier, err := goutils.CryptoRandomAlphaNumeric(64)
if err != nil {
return nil, werror.WrapWithContextParams(ctx, err, "failed to generate code verifier")
}
codeVerifierHash := sha256.Sum256([]byte(codeVerifier))
codeChallenge := base64.RawURLEncoding.EncodeToString(codeVerifierHash[:])
initialLoginURL, err := url.Parse(h.loginBaseURL)
if err != nil {
return nil, werror.WrapWithContextParams(ctx, err, "failed to parse login URL")
}
initialLoginURL.Path = path.Join(initialLoginURL.Path, authorizeApplicationPath)
initialLoginURL.RawQuery = url.Values{
"response_type": {"code"},
"client_id": {h.clientID},
"redirect_uri": {DefaultCallbackURL.String()},
"code_verifier": {codeVerifier},
"code_challenge": {codeChallenge},
"code_challenge_method": {"S256"},
}.Encode()
if err := browser.OpenURL(initialLoginURL.String()); err != nil {
return nil, werror.WrapWithContextParams(ctx, err, "failed to open browser for auth")
}

var code string
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-errorsCh:
return nil, werror.WrapWithContextParams(ctx, err, "could not complete auth handshake")
case code = <-resultsCh:
break
}

return &AuthorizationCode{
Code: code,
CodeVerifier: codeVerifier,
ClientID: h.clientID,
}, nil
}

func newRedirectHandler(resultsCh chan<- string, errorsCh chan<- error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("code")
if token == "" {
errorsCh <- errors.New("did not receive token")
}
if _, err := fmt.Fprint(w, "You have successfully signed into your account.\nYou can close this window and continue using the product."); err != nil {
errorsCh <- werror.Wrap(err, "failed to write response")
}
resultsCh <- token
}
}
63 changes: 63 additions & 0 deletions oauth/client_credentials_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) 2019 Palantir Technologies. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package oauth

import (
"context"
"net/http"
"net/url"

"github.com/palantir/conjure-go-runtime/v2/conjure-go-client/httpclient"
"github.com/palantir/conjure-go-runtime/v2/conjure-go-contract/codecs"
werror "github.com/palantir/witchcraft-go-error"
)

const (
clientCredentialsGrantType = "client_credentials"
)

type serviceClient struct {
client httpclient.Client
clientCredentialEndpoint string
}

// NewClientCredentialClient returns an oauth2.Client configured using the provided client.
// The client will use the httpclient's configured BaseURIs.
func NewClientCredentialClient(client httpclient.Client) ClientCredentialClient {
return &serviceClient{
client: client,
}
}

func (s *serviceClient) CreateClientCredentialToken(ctx context.Context, clientID, clientSecret string) (string, error) {
urlValues := url.Values{
"grant_type": []string{clientCredentialsGrantType},
"client_id": []string{clientID},
"client_secret": []string{clientSecret},
}
var oauth2Resp oauth2Response
_, err := s.client.Do(ctx,
httpclient.WithRPCMethodName("CreateClientCredentialToken"),
httpclient.WithRequestMethod(http.MethodPost),
httpclient.WithPath(oauthTokenEndpoint),
httpclient.WithRequestBody(urlValues, codecs.FormURLEncoded),
httpclient.WithJSONResponse(&oauth2Resp),
httpclient.WithRequestErrorDecoder(errorDecoder{ctx}),
)
if err != nil {
return "", werror.WrapWithContextParams(ctx, err, "failed to make create client credential token request")
}
return oauth2Resp.AccessToken, nil
}
File renamed without changes.
Loading