Skip to content

Commit

Permalink
Merge pull request #6 from superfly/multi-auth
Browse files Browse the repository at this point in the history
Allow different tokenizer auth for different providers
  • Loading branch information
btoews authored Oct 26, 2023
2 parents 0f3f2de + 8782a7b commit 68dc8a2
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 64 deletions.
54 changes: 47 additions & 7 deletions cmd/ssokenizer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"bytes"
"context"
"encoding/hex"
"errors"
"flag"
"fmt"
Expand All @@ -11,6 +12,7 @@ import (

"github.com/superfly/ssokenizer"
"github.com/superfly/ssokenizer/oauth2"
"github.com/superfly/tokenizer"
xoauth2 "golang.org/x/oauth2"
"golang.org/x/oauth2/amazon"
"golang.org/x/oauth2/bitbucket"
Expand Down Expand Up @@ -68,18 +70,45 @@ type Config struct {
// Tokenizer seal (public) key
SealKey string `yaml:"seal_key"`

// Auth key to put on tokenizer secrets
ProxyAuthorization string `yaml:"proxy_authorization"`

// Where to return user after auth dance. If present, the string `:name` is
// replaced with the provider name. Can also be specified per-provider.
ReturnURL string `yaml:"return_url"`

SecretAuth SecretAuthConfig `yaml:"secret_auth"`
Log LogConfig `yaml:"log"`
HTTP HTTPConfig `yaml:"http"`
IdentityProviders map[string]IdentityProviderConfig `yaml:"identity_providers"`
}

// Specifies what authentication clients should be required to present to
// tokenizer in order to use sealed secrets.
type SecretAuthConfig struct {
// The plain string that clients must pass in the Proxy-Authorization
// header.
Bearer string `yaml:"bearer"`

// Hex SHA256 digest of string that clients must pass in the
// Proxy-Authorization header.
BearerDigest string `yaml:"bearer_digest"`
}

func (c SecretAuthConfig) tokenizerAuthConfig() (tokenizer.AuthConfig, error) {
switch {
case c.Bearer != "" && c.BearerDigest != "":
return nil, errors.New("bearer and bearer_digest are mutually exclusive")
case c.Bearer != "":
return tokenizer.NewBearerAuthConfig(c.Bearer), nil
case c.BearerDigest != "":
d, err := hex.DecodeString(c.BearerDigest)
if err != nil {
return nil, err
}
return &tokenizer.BearerAuthConfig{Digest: d}, nil
default:
return nil, nil
}
}

// NewConfig returns a new instance of Config with defaults set.
func NewConfig() Config {
var config Config
Expand All @@ -88,17 +117,19 @@ func NewConfig() Config {

// Validate returns an error if the config is invalid.
func (c *Config) Validate() error {
if c.ProxyAuthorization == "" {
return errors.New("missing proxy_authorization")
tac, err := c.SecretAuth.tokenizerAuthConfig()
if err != nil {
return err
}

if c.SealKey == "" {
return errors.New("missing seal_key")
}
if c.HTTP.Address == "" {
return errors.New("missing http.address")
}
for _, pc := range c.IdentityProviders {
if err := pc.Validate(c.ReturnURL == ""); err != nil {
if err := pc.Validate(c.ReturnURL == "", tac == nil); err != nil {
return err
}
}
Expand Down Expand Up @@ -135,6 +166,8 @@ type IdentityProviderConfig struct {

// oauth token endpoint URL. Only needed for "oauth" profile
TokenURL string `yaml:"token_url"`

SecretAuth SecretAuthConfig `yaml:"secret_auth"`
}

func (c IdentityProviderConfig) providerConfig(name, returnURL string) (ssokenizer.ProviderConfig, error) {
Expand Down Expand Up @@ -250,14 +283,21 @@ func (c IdentityProviderConfig) providerConfig(name, returnURL string) (ssokeniz
}
}

func (c IdentityProviderConfig) Validate(needsReturnURL bool) error {
func (c IdentityProviderConfig) Validate(needsReturnURL, needsProxyAuthorization bool) error {
if c.Profile == "" {
return errors.New("missing identity_providers.profile")
}
if c.ReturnURL == "" && needsReturnURL {
return errors.New("missing return_url or identity_providers.return_url")
}

switch tac, err := c.SecretAuth.tokenizerAuthConfig(); {
case err != nil:
return err
case tac == nil && needsProxyAuthorization:
return errors.New("missing secret_auth or identity_providers.secret_auth")
}

return nil
}

Expand Down
10 changes: 8 additions & 2 deletions cmd/ssokenizer/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ Arguments:
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()

server := ssokenizer.NewServer(c.Config.SealKey, c.Config.ProxyAuthorization)
server := ssokenizer.NewServer(c.Config.SealKey)
serverTAC, _ := c.Config.SecretAuth.tokenizerAuthConfig()

for name, p := range c.Config.IdentityProviders {
returnURL := p.ReturnURL
Expand All @@ -81,11 +82,16 @@ Arguments:
returnURL = strings.ReplaceAll(returnURL, ":profile", p.Profile)
}

tac, _ := p.SecretAuth.tokenizerAuthConfig()
if tac == nil {
tac = serverTAC
}

pc, err := p.providerConfig(name, returnURL)
if err != nil {
return err
}
server.AddProvider(name, pc, returnURL)
server.AddProvider(name, pc, returnURL, tac)
}

if err := server.Start(c.Config.HTTP.Address); err != nil {
Expand Down
20 changes: 11 additions & 9 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@ type contextKey string

const (
contextKeyTransaction contextKey = "transaction"
contextKeyProvider contextKey = "provider"
)

func withTransaction(ctx context.Context, t *Transaction) context.Context {
return context.WithValue(ctx, contextKeyTransaction, t)
func withTransaction(r *http.Request, t *Transaction) *http.Request {
return r.WithContext(context.WithValue(r.Context(), contextKeyTransaction, t))
}

func GetTransaction(r *http.Request) (*Transaction, bool) {
return transactionFromContext(r.Context())
func GetTransaction(r *http.Request) *Transaction {
return r.Context().Value(contextKeyTransaction).(*Transaction)
}

func transactionFromContext(ctx context.Context) (*Transaction, bool) {
if t, ok := ctx.Value(contextKeyTransaction).(*Transaction); ok {
return t, true
}
return nil, false
func withProvider(r *http.Request, p *provider) *http.Request {
return r.WithContext(context.WithValue(r.Context(), contextKeyProvider, p))
}

func getProvider(r *http.Request) *provider {
return r.Context().Value(contextKeyProvider).(*provider)
}
25 changes: 21 additions & 4 deletions etc/ssokenizer.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
# Public part of tokenizer's keypair
seal_key: "$TOKENIZER_SEAL_KEY"

# Apps using sealed secrets with tokenizer will put this token in the
# `Proxy-Authorization` header.
proxy_authorization: "$PROXY_AUTH"

http:
# Where ssokenizer should listen
address: ":$PORT"
Expand All @@ -20,6 +16,11 @@ identity_providers:
# `google` here is the provider name. Users will go to
# https://<ssokenizer_url>/<provider_name>/start to start the oauth dance
google:
# Apps using sealed secrets with tokenizer will put this token in the
# `Proxy-Authorization` header.
secret_auth:
bearer: "$PROXY_AUTH"

# amazon, bitbucket, facebook, github, gitlab, google, heroku, microsoft,
# slack, or oauth.
profile: google
Expand All @@ -46,6 +47,8 @@ identity_providers:
# token_url: $PROVIDER_TOKEN_URL

github:
secret_auth:
bearer: "$PROXY_AUTH"
profile: github
client_id: "$GITHUB_CLIENT_ID"
client_secret: "$GITHUB_CLIENT_SECRET"
Expand All @@ -54,6 +57,8 @@ identity_providers:
- "$GITHUB_SCOPES"

heroku:
secret_auth:
bearer: "$PROXY_AUTH"
profile: heroku
client_id: "$HEROKU_CLIENT_ID"
client_secret: "$HEROKU_CLIENT_SECRET"
Expand All @@ -64,6 +69,8 @@ identity_providers:
# Same configurations except for name and return_url to allow authentication
# to staging environments with same OAuth client.
google_staging:
secret_auth:
bearer: "$PROXY_AUTH"
profile: google
client_id: "$GOOGLE_CLIENT_ID"
client_secret: "$GOOGLE_CLIENT_SECRET"
Expand All @@ -72,6 +79,8 @@ identity_providers:
- "$GOOGLE_SCOPES"

google_staging_2:
secret_auth:
bearer: "$PROXY_AUTH"
profile: google
client_id: "$GOOGLE_CLIENT_ID"
client_secret: "$GOOGLE_CLIENT_SECRET"
Expand All @@ -80,6 +89,8 @@ identity_providers:
- "$GOOGLE_SCOPES"

github_staging:
secret_auth:
bearer: "$PROXY_AUTH"
profile: github
client_id: "$GITHUB_CLIENT_ID"
client_secret: "$GITHUB_CLIENT_SECRET"
Expand All @@ -88,6 +99,8 @@ identity_providers:
- "$GITHUB_SCOPES"

github_staging_2:
secret_auth:
bearer: "$PROXY_AUTH"
profile: github
client_id: "$GITHUB_CLIENT_ID"
client_secret: "$GITHUB_CLIENT_SECRET"
Expand All @@ -96,6 +109,8 @@ identity_providers:
- "$GITHUB_SCOPES"

heroku_staging:
secret_auth:
bearer: "$PROXY_AUTH"
profile: heroku
client_id: "$HEROKU_CLIENT_ID"
client_secret: "$HEROKU_CLIENT_SECRET"
Expand All @@ -104,6 +119,8 @@ identity_providers:
- "$HEROKU_SCOPES"

heroku_staging_2:
secret_auth:
bearer: "$PROXY_AUTH"
profile: heroku
client_id: "$HEROKU_CLIENT_ID"
client_secret: "$HEROKU_CLIENT_SECRET"
Expand Down
5 changes: 2 additions & 3 deletions fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ processes = []
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 1
auto_stop_machines = false
auto_start_machines = false

[http_service.concurrency]
type = "requests"
Expand Down
27 changes: 8 additions & 19 deletions oauth2/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ type Config struct {

var _ ssokenizer.ProviderConfig = Config{}

func (c Config) Register(sealKey string, rpAuth string) (http.Handler, error) {
// implements ssokenizer.ProviderConfig
func (c Config) Register(sealKey string, auth tokenizer.AuthConfig) (http.Handler, error) {
switch {
case c.ClientID == "":
return nil, errors.New("missing client_id")
Expand All @@ -39,15 +40,15 @@ func (c Config) Register(sealKey string, rpAuth string) (http.Handler, error) {

return &provider{
sealKey: sealKey,
rpAuth: rpAuth,
auth: auth,
AllowedHostPattern: c.AllowedHostPattern,
configWithoutRedirectURL: c,
}, nil
}

type provider struct {
sealKey string
rpAuth string
auth tokenizer.AuthConfig
AllowedHostPattern string
configWithoutRedirectURL Config
}
Expand All @@ -72,24 +73,12 @@ func (p *provider) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

func (p *provider) handleStart(w http.ResponseWriter, r *http.Request) {
tr, ok := ssokenizer.GetTransaction(r)
if !ok {
logrus.Warn("no transaction for request")
w.WriteHeader(http.StatusInternalServerError)
return
}

tr := ssokenizer.GetTransaction(r)
http.Redirect(w, r, p.config(r).AuthCodeURL(tr.Nonce, oauth2.AccessTypeOffline), http.StatusFound)
}

func (p *provider) handleCallback(w http.ResponseWriter, r *http.Request) {
tr, ok := ssokenizer.GetTransaction(r)
if !ok {
logrus.Warn("no transaction for request")
w.WriteHeader(http.StatusInternalServerError)
return
}

tr := ssokenizer.GetTransaction(r)
params := r.URL.Query()

if errParam := params.Get("error"); errParam != "" {
Expand Down Expand Up @@ -131,7 +120,7 @@ func (p *provider) handleCallback(w http.ResponseWriter, r *http.Request) {
}

secret := &tokenizer.Secret{
AuthConfig: tokenizer.NewBearerAuthConfig(p.rpAuth),
AuthConfig: p.auth,
ProcessorConfig: &tokenizer.OAuthProcessorConfig{
Token: &tokenizer.OAuthToken{
AccessToken: tok.AccessToken,
Expand Down Expand Up @@ -174,7 +163,7 @@ func (p *provider) handleRefresh(w http.ResponseWriter, r *http.Request) {
}

secret := &tokenizer.Secret{
AuthConfig: tokenizer.NewBearerAuthConfig(p.rpAuth),
AuthConfig: p.auth,
ProcessorConfig: &tokenizer.OAuthProcessorConfig{
Token: &tokenizer.OAuthToken{
AccessToken: tok.AccessToken,
Expand Down
4 changes: 2 additions & 2 deletions oauth2/oauth2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestOauth2(t *testing.T) {
tkzServer := httptest.NewServer(tkz)
t.Cleanup(tkzServer.Close)

skz := ssokenizer.NewServer(sealKey, rpAuth)
skz := ssokenizer.NewServer(sealKey)
assert.NoError(t, skz.Start("127.0.0.1:"))
t.Logf("skz=http://%s", skz.Address)
t.Cleanup(func() {
Expand All @@ -68,7 +68,7 @@ func TestOauth2(t *testing.T) {
},
Scopes: []string{"my scope"},
},
}, rpServer.URL))
}, rpServer.URL, tokenizer.NewBearerAuthConfig(rpAuth)))

client := new(http.Client)
client.Jar, _ = cookiejar.New(nil)
Expand Down
10 changes: 9 additions & 1 deletion provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ package ssokenizer
import (
"net/http"
"net/url"

"github.com/superfly/tokenizer"
)

type provider struct {
name string
handler http.Handler
returnURL *url.URL
}

// Arbitrary configuration type for providers to implement.
type ProviderConfig interface {
Register(sealKey string, rpAuth string) (http.Handler, error)
// Register should validate the provider configuration and return a handler
// for requests to the provider. The provider can call GetTransaction to
// receive user state from the in-progress SSO transaction. The Transaction
// can be used to return data or error messages to the relying party.
Register(sealKey string, auth tokenizer.AuthConfig) (http.Handler, error)
}
Loading

0 comments on commit 68dc8a2

Please sign in to comment.