Skip to content

Commit

Permalink
oauth: get signing key from provider (keratin#236)
Browse files Browse the repository at this point in the history
Signing key can be included as a third optional segment in the colon-delimited environment variable strings used for credentials currently.  If not provided the existing global oauth signing key is used.
  • Loading branch information
AlexCuse authored Jan 19, 2024
1 parent f572515 commit 899a32b
Show file tree
Hide file tree
Showing 19 changed files with 274 additions and 194 deletions.
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

0 comments on commit 899a32b

Please sign in to comment.