-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add authentication support #31
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} |
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) | ||
} |
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) | ||
} |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not clear to me the purpose and scope of this method There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's a method required by the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should clarify a bit more in the comment to make it better! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment already states that it's used by |
||
return "Method" | ||
} |
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 | ||
} |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we not check whether the URL is a valid URL before setting it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not familiar with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validation at the moment happends inside |
||
o.Username = decoded.Username | ||
o.Password = decoded.Password | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a requirement on Password encoding, like minimum characters and specific characters? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess any such policy should be set on the service side rather than here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct -- this would be up to the particular OAuth2 service, we should not enforce anything here. |
||
|
||
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 | ||
} |
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" | ||
) | ||
|
||
yogeshbdeshpande marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, we should have a
Validate
Method on the IAuthenticator InterfaceThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To what end? Validation is performed as part of Configure() which is the only way for the user to alter the state of an
IAuthenticator
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True that this is part of Configure that you call Validator.
I am ok with this!