Skip to content
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

oauth: get signing key from provider #236

Merged
merged 4 commits into from
Jan 19, 2024
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## HEAD

### Added
* Ability to configure signing key per oauth provider.

## 1.18.0

Expand Down
40 changes: 22 additions & 18 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ import (
"github.com/go-redis/redis/v8"
"github.com/jmoiron/sqlx"
"github.com/keratin/authn-server/app/data"
dataRedis "github.com/keratin/authn-server/app/data/redis"
"github.com/keratin/authn-server/lib/oauth"
"github.com/keratin/authn-server/ops"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"

dataRedis "github.com/keratin/authn-server/app/data/redis"
)

type pinger func() bool
Expand Down Expand Up @@ -100,22 +99,7 @@ func NewApp(cfg *Config, logger logrus.FieldLogger) (*App, error) {
)
}

oauthProviders := map[string]oauth.Provider{}
if cfg.GoogleOauthCredentials != nil {
oauthProviders["google"] = *oauth.NewGoogleProvider(cfg.GoogleOauthCredentials)
}
if cfg.GitHubOauthCredentials != nil {
oauthProviders["github"] = *oauth.NewGitHubProvider(cfg.GitHubOauthCredentials)
}
if cfg.FacebookOauthCredentials != nil {
oauthProviders["facebook"] = *oauth.NewFacebookProvider(cfg.FacebookOauthCredentials)
}
if cfg.DiscordOauthCredentials != nil {
oauthProviders["discord"] = *oauth.NewDiscordProvider(cfg.DiscordOauthCredentials)
}
if cfg.MicrosoftOauthCredientials != nil {
oauthProviders["microsoft"] = *oauth.NewMicrosoftProvider(cfg.MicrosoftOauthCredientials)
}
oauthProviders := initializeOAuthProviders(cfg)

return &App{
// Provide access to root DB - useful when extending AccountStore functionality
Expand All @@ -133,3 +117,23 @@ func NewApp(cfg *Config, logger logrus.FieldLogger) (*App, error) {
Logger: logger,
}, nil
}

func initializeOAuthProviders(cfg *Config) map[string]oauth.Provider {
oauthProviders := make(map[string]oauth.Provider)
if cfg.GoogleOauthCredentials != nil {
oauthProviders["google"] = *oauth.NewGoogleProvider(cfg.GoogleOauthCredentials)
}
if cfg.GitHubOauthCredentials != nil {
oauthProviders["github"] = *oauth.NewGitHubProvider(cfg.GitHubOauthCredentials)
}
if cfg.FacebookOauthCredentials != nil {
oauthProviders["facebook"] = *oauth.NewFacebookProvider(cfg.FacebookOauthCredentials)
}
if cfg.DiscordOauthCredentials != nil {
oauthProviders["discord"] = *oauth.NewDiscordProvider(cfg.DiscordOauthCredentials)
}
if cfg.MicrosoftOauthCredentials != nil {
oauthProviders["microsoft"] = *oauth.NewMicrosoftProvider(cfg.MicrosoftOauthCredentials)
}
return oauthProviders
}
36 changes: 18 additions & 18 deletions app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ type Config struct {
GitHubOauthCredentials *oauth.Credentials
FacebookOauthCredentials *oauth.Credentials
DiscordOauthCredentials *oauth.Credentials
MicrosoftOauthCredientials *oauth.Credentials
MicrosoftOauthCredentials *oauth.Credentials
RefreshTokenExplicitExpiry bool
}

Expand All @@ -86,7 +86,7 @@ func (c *Config) OAuthEnabled() bool {
c.GitHubOauthCredentials != nil ||
c.FacebookOauthCredentials != nil ||
c.DiscordOauthCredentials != nil ||
c.MicrosoftOauthCredientials != nil
c.MicrosoftOauthCredentials != nil
}

// SameSiteComputed returns either the specified http.SameSite, or a computed one from OAuth config
Expand Down Expand Up @@ -586,11 +586,11 @@ var configurers = []configurer{
return nil
},

// GOOGLE_OAUTH_CREDENTIALS is a credential pair in the format `id:secret`. When specified,
// AuthN will enable routes for Google OAuth signin.
// GOOGLE_OAUTH_CREDENTIALS is a credential pair in the format `id:secret:signing_key(optional)`.
// When specified, AuthN will enable routes for Google OAuth signin.
func(c *Config) error {
if val, ok := os.LookupEnv("GOOGLE_OAUTH_CREDENTIALS"); ok {
credentials, err := oauth.NewCredentials(val)
credentials, err := oauth.NewCredentials(val, c.OAuthSigningKey)
if err == nil {
c.GoogleOauthCredentials = credentials
}
Expand All @@ -599,11 +599,11 @@ var configurers = []configurer{
return nil
},

// GITHUB_OAUTH_CREDENTIALS is a credential pair in the format `id:secret`. When specified,
// AuthN will enable routes for GitHub OAuth signin.
// GITHUB_OAUTH_CREDENTIALS is a credential pair in the format `id:secret:signing_key(optional)`.
// When specified, AuthN will enable routes for GitHub OAuth signin.
func(c *Config) error {
if val, ok := os.LookupEnv("GITHUB_OAUTH_CREDENTIALS"); ok {
credentials, err := oauth.NewCredentials(val)
credentials, err := oauth.NewCredentials(val, c.OAuthSigningKey)
if err == nil {
c.GitHubOauthCredentials = credentials
}
Expand All @@ -612,11 +612,11 @@ var configurers = []configurer{
return nil
},

// FACEBOOK_OAUTH_CREDENTIALS is a credential pair in the format `id:secret`. When specified,
// AuthN will enable routes for Facebook OAuth signin.
// FACEBOOK_OAUTH_CREDENTIALS is a credential pair in the format `id:secret:signing_key(optional)`.
// When specified, AuthN will enable routes for Facebook OAuth signin.
func(c *Config) error {
if val, ok := os.LookupEnv("FACEBOOK_OAUTH_CREDENTIALS"); ok {
credentials, err := oauth.NewCredentials(val)
credentials, err := oauth.NewCredentials(val, c.OAuthSigningKey)
if err == nil {
c.FacebookOauthCredentials = credentials
}
Expand All @@ -625,11 +625,11 @@ var configurers = []configurer{
return nil
},

// DISCORD_OAUTH_CREDENTIALS is a credential pair in the format `id:secret`. When specified,
// AuthN will enable routes for Discord OAuth signin.
// DISCORD_OAUTH_CREDENTIALS is a credential pair in the format `id:secret:signing_key(optional)`.
// When specified, AuthN will enable routes for Discord OAuth signin.
func(c *Config) error {
if val, ok := os.LookupEnv("DISCORD_OAUTH_CREDENTIALS"); ok {
credentials, err := oauth.NewCredentials(val)
credentials, err := oauth.NewCredentials(val, c.OAuthSigningKey)
if err == nil {
c.DiscordOauthCredentials = credentials
}
Expand All @@ -638,13 +638,13 @@ var configurers = []configurer{
return nil
},

// Microsoft_OAUTH_CREDENTIALS is a credential pair in the format `id:secret`. When specified,
// AuthN will enable routes for Discord OAuth signin.
// Microsoft_OAUTH_CREDENTIALS is a credential pair in the format `id:secret:signing_key(optional)`.
// When specified, AuthN will enable routes for Microsoft OAuth signin.
func(c *Config) error {
if val, ok := os.LookupEnv("MICROSOFT_OAUTH_CREDENTIALS"); ok {
credentials, err := oauth.NewCredentials(val)
credentials, err := oauth.NewCredentials(val, c.OAuthSigningKey)
if err == nil {
c.MicrosoftOauthCredientials = credentials
c.MicrosoftOauthCredentials = credentials
}
return err
}
Expand Down
6 changes: 3 additions & 3 deletions app/tokens/oauth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ type Claims struct {
}

// Sign converts the claims into a serialized string, signed with HMAC.
func (c *Claims) Sign(hmacKey []byte) (string, error) {
func (c *Claims) Sign(signingKey jose.SigningKey) (string, error) {
signer, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.HS256, Key: hmacKey},
signingKey,
(&jose.SignerOptions{}).WithType("JWT"),
)
if err != nil {
Expand Down Expand Up @@ -68,7 +68,7 @@ func Parse(tokenStr string, cfg *app.Config, nonce string) (*Claims, error) {
// New creates Claims for a JWT suitable as a state parameter during an OAuth flow.
func New(cfg *app.Config, nonce string, destination string) (*Claims, error) {
return &Claims{
Scope: scope,
Scope: scope,
RequestForgeryProtection: nonce,
Destination: destination,
Claims: jwt.Claims{
Expand Down
9 changes: 5 additions & 4 deletions app/tokens/oauth/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/keratin/authn-server/app/tokens/oauth"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2"
)

func TestOAuthToken(t *testing.T) {
Expand All @@ -28,7 +29,7 @@ func TestOAuthToken(t *testing.T) {
assert.True(t, token.Audience.Contains("https://authn.example.com"))
assert.NotEmpty(t, token.IssuedAt)

tokenStr, err := token.Sign(cfg.OAuthSigningKey)
tokenStr, err := token.Sign(jose.SigningKey{Algorithm: jose.HS256, Key: cfg.OAuthSigningKey})
require.NoError(t, err)

_, err = oauth.Parse(tokenStr, cfg, nonce)
Expand All @@ -39,7 +40,7 @@ func TestOAuthToken(t *testing.T) {
token, err := oauth.New(cfg, nonce, "https://app.example.com/return")
require.NoError(t, err)

tokenStr, err := token.Sign(cfg.OAuthSigningKey)
tokenStr, err := token.Sign(jose.SigningKey{Algorithm: jose.HS256, Key: cfg.OAuthSigningKey})
require.NoError(t, err)

_, err = oauth.Parse(tokenStr, cfg, "wrong")
Expand All @@ -53,7 +54,7 @@ func TestOAuthToken(t *testing.T) {
}
token, err := oauth.New(cfg, nonce, "https://app.example.com/return")
require.NoError(t, err)
tokenStr, err := token.Sign(oldCfg.OAuthSigningKey)
tokenStr, err := token.Sign(jose.SigningKey{Algorithm: jose.HS256, Key: oldCfg.OAuthSigningKey})
require.NoError(t, err)
_, err = oauth.Parse(tokenStr, cfg, nonce)
assert.Error(t, err)
Expand All @@ -66,7 +67,7 @@ func TestOAuthToken(t *testing.T) {
}
token, err := oauth.New(&oldCfg, nonce, "https://app.example.com/return")
require.NoError(t, err)
tokenStr, err := token.Sign(cfg.OAuthSigningKey)
tokenStr, err := token.Sign(jose.SigningKey{Algorithm: jose.HS256, Key: cfg.OAuthSigningKey})
require.NoError(t, err)
_, err = oauth.Parse(tokenStr, cfg, nonce)
assert.Error(t, err)
Expand Down
52 changes: 27 additions & 25 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,53 +261,55 @@ or

* `https://www.example.com/authn/oauth/google/return`

By default OAuth providers will use a key derived from `SECRET_KEY_BASE`. To override you can provide a hex-encoded string as the third segment in the colon-delimited environment variable.

### `FACEBOOK_OAUTH_CREDENTIALS`

| | |
| --------- | --- |
| Required? | No |
| Value | AppID:AppSecret |
| Default | nil |
| | |
|-----------|--------------------------------------|
| Required? | No |
| Value | AppID:AppSecret:SigningKey(optional) |
| Default | nil |

Create a Facebook app at https://developers.facebook.com and enable the Facebook Login product. In the Quickstart, enter [AuthN's OAuth Return](api.md#oauth-return) as the Site URL. Then switch over to Settings and find the App ID and Secret. Join those together with a `:` and provide them to AuthN as a single variable.

### `GITHUB_OAUTH_CREDENTIALS`

| | |
| --------- | --- |
| Required? | No |
| Value | ClientID:ClientSecret |
| Default | nil |
| | |
|-----------|--------------------------------------|
| Required? | No |
| Value | AppID:AppSecret:SigningKey(optional) |
| Default | nil |

Sign up for GitHub OAuth 2.0 credentials with the instructions here: https://developer.github.com/apps/building-oauth-apps. Your client's ID and secret must be joined together with a `:` and provided to AuthN as a single variable.

### `GOOGLE_OAUTH_CREDENTIALS`

| | |
| --------- | --- |
| Required? | No |
| Value | ClientID:ClientSecret |
| Default | nil |
| | |
|-----------|--------------------------------------|
| Required? | No |
| Value | AppID:AppSecret:SigningKey(optional) |
| Default | nil |

Sign up for Google OAuth 2.0 credentials with the instructions here: https://developers.google.com/identity/protocols/OpenIDConnect. Your client's ID and secret must be joined together with a `:` and provided to AuthN as a single variable.

### `DISCORD_OAUTH_CREDENTIALS`

| | |
| --------- | --- |
| Required? | No |
| Value | ClientID:ClientSecret |
| Default | nil |
| | |
|-----------|--------------------------------------|
| Required? | No |
| Value | AppID:AppSecret:SigningKey(optional) |
| Default | nil |

Sign up for Discord OAuth 2.0 credentials with the instructions here: https://discordapp.com/developers/docs/topics/oauth2. Your client's ID and secret must be joined together with a `:` and provided to AuthN as a single variable.

### `MICROSOFT_OAUTH_CREDENTIALS`

| | |
| --------- | --- |
| Required? | No |
| Value | ClientID:ClientSecret |
| Default | nil |
| | |
|-----------|--------------------------------------|
| Required? | No |
| Value | AppID:AppSecret:SigningKey(optional) |
| Default | nil |

Sign up for Microsoft OAuth 2.0 credentials with the instructions here: https://docs.microsoft.com/fr-fr/graph/auth/. Your client's ID and secret must be joined together with a `:` and provided to AuthN as a single variable.

Expand Down
36 changes: 26 additions & 10 deletions lib/oauth/credentials.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
package oauth

import (
"encoding/hex"
"errors"
"fmt"
"strings"
)

// Credentials is a configuration struct for OAuth Providers
type Credentials struct {
ID string
Secret string
ID string
Secret string
SigningKey []byte
}

// NewCredentials parses a credential string in the format `id:string` and returns a Credentials
// suitable for OAuth Provider configuration.
func NewCredentials(credentials string) (*Credentials, error) {
if strings.Count(credentials, ":") != 1 {
return nil, errors.New("Credentials must be in the format `id:string`")
// NewCredentials parses a credential string in the format `id:string:signing_key(optional)`
// and returns a Credentials suitable for OAuth Provider configuration. If no signing key is
// provided the default key is used.
func NewCredentials(credentials string, defaultKey []byte) (*Credentials, error) {
if strings.Count(credentials, ":") < 1 {
return nil, errors.New("Credentials must be in the format `id:string:signing_key(optional)`")
}
strs := strings.SplitN(credentials, ":", 2)
return &Credentials{
strs := strings.SplitN(credentials, ":", 3)

c := &Credentials{
ID: strs[0],
Secret: strs[1],
}, nil
}

if len(strs) == 3 {
key, err := hex.DecodeString(strs[2])
if err != nil {
return nil, fmt.Errorf("failed to decode signing key: %w", err)
}
c.SigningKey = key
} else {
c.SigningKey = defaultKey
}
return c, nil
}
Loading
Loading