-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for authentication with the remote service. Basic HTTP and OAuth2 schemes are supported (as well as "passthrough" which disables authentication). This is implemented by associating an IAuthorizer with the Client, which sets the Authrization HTTP header in Client's requests. Signed-off-by: Sergei Trofimov <[email protected]>
- Loading branch information
Showing
16 changed files
with
466 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// Copyright 2023 Contributors to the Veraison project. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package auth | ||
|
||
import ( | ||
"encoding/base64" | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/mitchellh/mapstructure" | ||
) | ||
|
||
type BasicAuthenticator struct { | ||
Username string | ||
Password string | ||
} | ||
|
||
func (o *BasicAuthenticator) Configure(cfg map[string]interface{}) error { | ||
decoded := struct { | ||
Username string `mapstructure:"username"` | ||
Password string `mapstructure:"password"` | ||
Rest map[string]interface{} `mapstructure:",remain"` | ||
}{} | ||
|
||
if err := mapstructure.Decode(cfg, &decoded); err != nil { | ||
return err | ||
} | ||
|
||
o.Username = decoded.Username | ||
o.Password = decoded.Password | ||
|
||
if err := o.validate(); err != nil { | ||
return err | ||
} | ||
|
||
if len(decoded.Rest) > 0 { | ||
var unexpected []string | ||
for k := range decoded.Rest { | ||
unexpected = append(unexpected, k) | ||
} | ||
return fmt.Errorf("unexpected fields in config: %s", | ||
strings.Join(unexpected, ", ")) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (o *BasicAuthenticator) EncodeHeader() (string, error) { | ||
if err := o.validate(); err != nil { | ||
return "", err | ||
} | ||
|
||
credsRaw := fmt.Sprintf("%s:%s", o.Username, o.Password) | ||
credsEncoded := base64.StdEncoding.EncodeToString([]byte(credsRaw)) | ||
header := fmt.Sprintf("Basic %s", credsEncoded) | ||
|
||
return header, nil | ||
} | ||
|
||
func (o *BasicAuthenticator) validate() error { | ||
if o.Username == "" { | ||
return errors.New("missing username") | ||
} | ||
|
||
if o.Password == "" { | ||
return errors.New("missing password") | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package auth | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestBasic_Configure(t *testing.T) { | ||
var ba BasicAuthenticator | ||
|
||
err := ba.Configure(map[string]interface{}{ | ||
"username": "user1", | ||
"password": "Passw0rd!", | ||
}) | ||
require.NoError(t, err) | ||
assert.Equal(t, "user1", ba.Username) | ||
assert.Equal(t, "Passw0rd!", ba.Password) | ||
|
||
err = ba.Configure(map[string]interface{}{ | ||
"username": "user1", | ||
}) | ||
assert.EqualError(t, err, "missing password") | ||
|
||
err = ba.Configure(map[string]interface{}{ | ||
"password": "Passw0rd!", | ||
}) | ||
assert.EqualError(t, err, "missing username") | ||
|
||
err = ba.Configure(map[string]interface{}{ | ||
"username": "user1", | ||
"password": "Passw0rd!", | ||
"full name": "User One", | ||
}) | ||
assert.EqualError(t, err, "unexpected fields in config: full name") | ||
} | ||
|
||
func TestBasic_EncodeHeader(t *testing.T) { | ||
var ba BasicAuthenticator | ||
|
||
_, err := ba.EncodeHeader() | ||
assert.EqualError(t, err, "missing username") | ||
|
||
err = ba.Configure(map[string]interface{}{ | ||
"username": "user1", | ||
"password": "Passw0rd!", | ||
}) | ||
require.NoError(t, err) | ||
|
||
header, err := ba.EncodeHeader() | ||
require.NoError(t, err) | ||
assert.Equal(t, "Basic dXNlcjE6UGFzc3cwcmQh", header) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// Copyright 2023 Contributors to the Veraison project. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package auth | ||
|
||
type IAuthenticator interface { | ||
Configure(cfg map[string]interface{}) error | ||
EncodeHeader() (string, error) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// Copyright 2023 Contributors to the Veraison project. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package auth | ||
|
||
import "fmt" | ||
|
||
// Method is the enumeration of authentication methods supported by Veraison | ||
// service. It implements the pflag.Value interface. | ||
type Method string | ||
|
||
const ( | ||
MethodPassthrough Method = "passthrough" | ||
MethodBasic Method = "basic" | ||
MethodOauth2 Method = "oauth2" | ||
) | ||
|
||
// String representation of the Method | ||
func (o *Method) String() string { | ||
return string(*o) | ||
} | ||
|
||
// Set the value of the Method | ||
func (o *Method) Set(v string) error { | ||
switch v { | ||
case "none", "passthrough": | ||
*o = MethodPassthrough | ||
case "basic": | ||
*o = MethodBasic | ||
case "oauth2": | ||
*o = MethodOauth2 | ||
default: | ||
return fmt.Errorf("unexpected Method %q", v) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Type returns the string representing the type name (used by pflag). | ||
func (o *Method) Type() string { | ||
return "Method" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// Copyright 2023 Contributors to the Veraison project. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package auth | ||
|
||
type NullAuthenticator struct{} | ||
|
||
func (o *NullAuthenticator) Configure(cfg map[string]interface{}) error { | ||
return nil | ||
} | ||
|
||
func (o *NullAuthenticator) EncodeHeader() (string, error) { | ||
return "", nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
// Copyright 2023 Contributors to the Veraison project. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package auth | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"net/url" | ||
"strings" | ||
"time" | ||
|
||
"github.com/mitchellh/mapstructure" | ||
"golang.org/x/oauth2" | ||
) | ||
|
||
type Oauth2Authenticator struct { | ||
TokenURL string | ||
ClientID string | ||
ClientSecret string | ||
Username string | ||
Password string | ||
|
||
Token *oauth2.Token | ||
} | ||
|
||
func (o *Oauth2Authenticator) Configure(cfg map[string]interface{}) error { | ||
decoded := struct { | ||
TokenURL string `mapstructure:"token_url" valid:"url"` | ||
ClientID string `mapstructure:"client_id"` | ||
ClientSecret string `mapstructure:"client_secret"` | ||
Username string `mapstructure:"username"` | ||
Password string `mapstructure:"password"` | ||
Rest map[string]interface{} `mapstructure:",remain"` | ||
}{} | ||
|
||
if err := mapstructure.Decode(cfg, &decoded); err != nil { | ||
return err | ||
} | ||
|
||
o.ClientID = decoded.ClientID | ||
o.ClientSecret = decoded.ClientSecret | ||
o.TokenURL = decoded.TokenURL | ||
o.Username = decoded.Username | ||
o.Password = decoded.Password | ||
|
||
if err := o.validate(); err != nil { | ||
return err | ||
} | ||
|
||
if len(decoded.Rest) > 0 { | ||
var unexpected []string | ||
for k := range decoded.Rest { | ||
unexpected = append(unexpected, k) | ||
} | ||
return fmt.Errorf("unexpected fields in config: %s", | ||
strings.Join(unexpected, ", ")) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (o *Oauth2Authenticator) EncodeHeader() (string, error) { | ||
var err error | ||
|
||
if o.Token == nil || o.Token.Expiry.Before(time.Now()) { | ||
o.Token, err = o.obtainToken() | ||
if err != nil { | ||
return "", err | ||
} | ||
} | ||
|
||
header := fmt.Sprintf("Bearer %s", o.Token.AccessToken) | ||
|
||
return header, nil | ||
} | ||
|
||
func (o *Oauth2Authenticator) obtainToken() (*oauth2.Token, error) { | ||
if err := o.validate(); err != nil { | ||
return nil, err | ||
} | ||
|
||
ctx := context.Background() | ||
conf := &oauth2.Config{ | ||
ClientID: o.ClientID, | ||
ClientSecret: o.ClientSecret, | ||
Scopes: []string{"openid"}, | ||
Endpoint: oauth2.Endpoint{ | ||
TokenURL: o.TokenURL, | ||
}, | ||
} | ||
|
||
return conf.PasswordCredentialsToken(ctx, o.Username, o.Password) | ||
} | ||
|
||
func (o *Oauth2Authenticator) validate() error { | ||
if o.ClientID == "" { | ||
return errors.New("missing client_id") | ||
} | ||
|
||
if o.ClientSecret == "" { | ||
return errors.New("missing client_secret") | ||
} | ||
|
||
if o.TokenURL == "" { | ||
return errors.New("missing token_url") | ||
} | ||
|
||
if _, err := url.Parse(o.TokenURL); err != nil { | ||
return fmt.Errorf("invalid token_url: %w", err) | ||
} | ||
|
||
if o.Username == "" { | ||
return errors.New("missing username") | ||
} | ||
|
||
if o.Password == "" { | ||
return errors.New("missing password") | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package auth | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestOauth2_Configure(t *testing.T) { | ||
var oa2a Oauth2Authenticator | ||
|
||
err := oa2a.Configure(map[string]interface{}{ | ||
"client_id": "myclient", | ||
"client_secret": "deadbeef", | ||
"username": "user1", | ||
"password": "Passw0rd!", | ||
"token_url": "http://example.com", | ||
}) | ||
require.NoError(t, err) | ||
assert.Equal(t, "user1", oa2a.Username) | ||
assert.Equal(t, "Passw0rd!", oa2a.Password) | ||
assert.Equal(t, "myclient", oa2a.ClientID) | ||
assert.Equal(t, "deadbeef", oa2a.ClientSecret) | ||
assert.Equal(t, "http://example.com", oa2a.TokenURL) | ||
|
||
err = oa2a.Configure(map[string]interface{}{ | ||
"client_id": "myclient", | ||
"client_secret": "deadbeef", | ||
"username": "user1", | ||
"token_url": "http://example.com", | ||
}) | ||
assert.EqualError(t, err, "missing password") | ||
|
||
err = oa2a.Configure(map[string]interface{}{ | ||
"client_id": "myclient", | ||
"client_secret": "deadbeef", | ||
"token_url": "http://example.com", | ||
"password": "Passw0rd!", | ||
}) | ||
assert.EqualError(t, err, "missing username") | ||
|
||
err = oa2a.Configure(map[string]interface{}{ | ||
"client_id": "myclient", | ||
"client_secret": "deadbeef", | ||
"username": "user1", | ||
"password": "Passw0rd!", | ||
"token_url": "http://example.com", | ||
"full name": "User One", | ||
}) | ||
assert.EqualError(t, err, "unexpected fields in config: full name") | ||
} |
Oops, something went wrong.