Skip to content

Commit

Permalink
Add authentication support
Browse files Browse the repository at this point in the history
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
setrofim committed Sep 5, 2023
1 parent 07ba530 commit 630cea1
Show file tree
Hide file tree
Showing 16 changed files with 466 additions and 28 deletions.
71 changes: 71 additions & 0 deletions auth/basic.go
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
}
54 changes: 54 additions & 0 deletions auth/basic_test.go
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)
}
8 changes: 8 additions & 0 deletions auth/iauthenticator.go
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)
}
42 changes: 42 additions & 0 deletions auth/method.go
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"
}
13 changes: 13 additions & 0 deletions auth/null.go
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
}
122 changes: 122 additions & 0 deletions auth/oauth2.go
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
}
52 changes: 52 additions & 0 deletions auth/oauth2_test.go
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")
}
Loading

0 comments on commit 630cea1

Please sign in to comment.