Skip to content

Commit

Permalink
client_id and client_secret not required if client is authenticated v…
Browse files Browse the repository at this point in the history
…ia JWT assertion
  • Loading branch information
Johannes Koch committed Aug 15, 2022
1 parent 758fa31 commit 6bd0c0a
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 25 deletions.
4 changes: 4 additions & 0 deletions config/ac_oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ func (oa *OAuth2AC) GetName() string {
return oa.Name
}

func (oa *OAuth2AC) ClientAuthenticationRequired() bool {
return true
}

func (oa *OAuth2AC) GetClientID() string {
return oa.ClientID
}
Expand Down
4 changes: 4 additions & 0 deletions config/ac_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ func (o *OIDC) GetName() string {
return o.Name
}

func (o *OIDC) ClientAuthenticationRequired() bool {
return true
}

func (o *OIDC) GetClientID() string {
return o.ClientID
}
Expand Down
1 change: 1 addition & 0 deletions config/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type OidcAS interface {
// OAuth2Client represents the client configuration for OAuth2 clients.
type OAuth2Client interface {
Inline
ClientAuthenticationRequired() bool
GetClientID() string
GetClientSecret() string
GetTokenEndpointAuthMethod() *string
Expand Down
14 changes: 12 additions & 2 deletions config/oauth2ra.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import (
"github.com/hashicorp/hcl/v2/gohcl"
)

const (
ClientCredentials = "client_credentials"
JwtBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer"
Password = "password"
)

var OAuthBlockSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Expand All @@ -24,8 +30,8 @@ var (
type OAuth2ReqAuth struct {
AssertionExpr hcl.Expression `hcl:"assertion,optional"`
BackendName string `hcl:"backend,optional"`
ClientID string `hcl:"client_id"`
ClientSecret string `hcl:"client_secret"`
ClientID string `hcl:"client_id,optional"`
ClientSecret string `hcl:"client_secret,optional"`
GrantType string `hcl:"grant_type"`
Password string `hcl:"password,optional"`
Remain hcl.Body `hcl:",remain"`
Expand Down Expand Up @@ -72,6 +78,10 @@ func (oa *OAuth2ReqAuth) Schema(inline bool) *hcl.BodySchema {
return schema
}

func (oa *OAuth2ReqAuth) ClientAuthenticationRequired() bool {
return oa.GrantType != JwtBearer
}

func (oa *OAuth2ReqAuth) GetClientID() string {
return oa.ClientID
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ The `oauth2` block in the [Backend Block](backend) context configures an OAuth2
| `backend` | string | - | [Backend Block Reference](backend) | - | - |
| `grant_type` | string | - | - | ⚠ required, valid values: `"client_credentials"`, `"password"`, `"urn:ietf:params:oauth:grant-type:jwt-bearer"` | `grant_type = "client_credentials"` |
| `token_endpoint` | string | - | URL of the token endpoint at the authorization server. | ⚠ required | - |
| `client_id` | string | - | The client identifier. | ⚠ required | - |
| `client_secret` | string | - | The client password. | ⚠ required. | - |
| `client_id` | string | - | The client identifier. | ⚠ required unless the client is authenticated via the JWT assertion with `grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"`. | - |
| `client_secret` | string | - | The client password. | ⚠ required unless the client is authenticated via the JWT assertion with `grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"`. | - |
| `retries` | integer | `1` | The number of retries to get the token and resource, if the resource-request responds with `401 Unauthorized` HTTP status code. | - | - |
| `token_endpoint_auth_method` | string | `"client_secret_basic"` | Defines the method to authenticate the client at the token endpoint. | If set to `"client_secret_post"`, the client credentials are transported in the request body. If set to `"client_secret_basic"`, the client credentials are transported via Basic Authentication. | - |
| `scope` | string | - | A space separated list of requested scope values for the access token. | - | `scope = "read write"` |
Expand Down
27 changes: 15 additions & 12 deletions handler/transport/oauth2_req_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,10 @@ import (
"github.com/avenga/couper/oauth2"
)

const (
clientCredentials = "client_credentials"
jwtBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer"
password = "password"
)

var supportedGrantTypes = map[string]struct{}{
clientCredentials: struct{}{},
jwtBearer: struct{}{},
password: struct{}{},
config.ClientCredentials: struct{}{},
config.JwtBearer: struct{}{},
config.Password: struct{}{},
}

// OAuth2ReqAuth represents the transport <OAuth2ReqAuth> object.
Expand All @@ -47,7 +41,7 @@ func NewOAuth2ReqAuth(evalCtx *hcl.EvalContext, conf *config.OAuth2ReqAuth, memS
return nil, fmt.Errorf("grant_type %s not supported", conf.GrantType)
}

if conf.GrantType == password {
if conf.GrantType == config.Password {
if conf.Username == "" {
return nil, fmt.Errorf("username must not be empty with grant_type=password")
}
Expand All @@ -68,7 +62,7 @@ func NewOAuth2ReqAuth(evalCtx *hcl.EvalContext, conf *config.OAuth2ReqAuth, memS
return nil, err
}

if conf.GrantType == jwtBearer {
if conf.GrantType == config.JwtBearer {
if assertionValue.IsNull() && assertionValue.Type() == cty.DynamicPseudoType {
return nil, fmt.Errorf("missing assertion with grant_type=%s", conf.GrantType)
}
Expand All @@ -78,6 +72,15 @@ func NewOAuth2ReqAuth(evalCtx *hcl.EvalContext, conf *config.OAuth2ReqAuth, memS
}
}

if conf.ClientAuthenticationRequired() {
if conf.ClientID == "" {
return nil, fmt.Errorf("client_id must not be empty")
}
if conf.ClientSecret == "" {
return nil, fmt.Errorf("client_secret must not be empty")
}
}

oauth2Client, err := oauth2.NewClient(conf.GrantType, conf, conf, asBackend)
if err != nil {
return nil, err
Expand All @@ -102,7 +105,7 @@ func (oa *OAuth2ReqAuth) GetToken(req *http.Request) error {

formParams := url.Values{}

if oa.config.GrantType == jwtBearer {
if oa.config.GrantType == config.JwtBearer {
if assertionValue.IsNull() {
return fmt.Errorf("null assertion with grant_type=%s", oa.config.GrantType)
} else if assertionValue.Type() != cty.String {
Expand Down
4 changes: 4 additions & 0 deletions oauth2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ func (c *Client) newTokenRequest(ctx context.Context, formParams url.Values) (*h
}

func authenticateClient(clientConfig config.OAuth2Client, formParams *url.Values, tokenReq *http.Request) {
if !clientConfig.ClientAuthenticationRequired() {
return
}

clientID := clientConfig.GetClientID()
clientSecret := clientConfig.GetClientSecret()
if authMethod := clientConfig.GetTokenEndpointAuthMethod(); authMethod == nil || *authMethod == "client_secret_basic" {
Expand Down
72 changes: 65 additions & 7 deletions server/http_oauth2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func TestEndpoints_OAuth2_Options(t *testing.T) {
{
"16_couper.hcl",
`assertion=GET&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer`,
"Basic bXlfY2xpZW50Om15X2NsaWVudF9zZWNyZXQ=",
"",
},
} {
var tokenSeenCh chan struct{}
Expand Down Expand Up @@ -291,6 +291,70 @@ func TestOAuth2_Config_Errors(t *testing.T) {
}

for _, tc := range []testCase{
{
"grant_type client_credentials without client_id",
`server {}
definitions {
backend "be" {
oauth2 {
token_endpoint = "https://authorization.server/token"
client_secret = "my_client_secret"
grant_type = "client_credentials"
}
}
}
`,
"configuration error: be: client_id must not be empty",
},
{
"grant_type client_credentials without client_secret",
`server {}
definitions {
backend "be" {
oauth2 {
token_endpoint = "https://authorization.server/token"
client_id = "my_client"
grant_type = "client_credentials"
}
}
}
`,
"configuration error: be: client_secret must not be empty",
},
{
"grant_type password without client_id",
`server {}
definitions {
backend "be" {
oauth2 {
token_endpoint = "https://authorization.server/token"
client_secret = "my_client_secret"
grant_type = "password"
username = "my_user"
password = "my_password"
}
}
}
`,
"configuration error: be: client_id must not be empty",
},
{
"grant_type password without client_secret",
`server {}
definitions {
backend "be" {
oauth2 {
token_endpoint = "https://authorization.server/token"
client_id = "my_client"
grant_type = "password"
username = "my_user"
password = "my_password"
}
}
}
`,
"configuration error: be: client_secret must not be empty",
},
{
"username with grant_type client_credentials",
`server {}
Expand Down Expand Up @@ -332,8 +396,6 @@ definitions {
backend "be" {
oauth2 {
token_endpoint = "https://authorization.server/token"
client_id = "my_client"
client_secret = "my_client_secret"
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"
username = "my_user"
}
Expand All @@ -349,8 +411,6 @@ definitions {
backend "be" {
oauth2 {
token_endpoint = "https://authorization.server/token"
client_id = "my_client"
client_secret = "my_client_secret"
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"
password = "my_password"
}
Expand Down Expand Up @@ -435,8 +495,6 @@ definitions {
backend "be" {
oauth2 {
token_endpoint = "https://authorization.server/token"
client_id = "my_client"
client_secret = "my_client_secret"
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"
}
}
Expand Down
2 changes: 0 additions & 2 deletions server/testdata/oauth2/16_couper.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ server {
backend {
oauth2 {
token_endpoint = "{{.asOrigin}}/options"
client_id = "my_client"
client_secret = "my_client_secret"
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"
assertion = request.method # easier for test purpose, should of course be a signed JWT
}
Expand Down

0 comments on commit 6bd0c0a

Please sign in to comment.