Skip to content

Commit

Permalink
Initial Vanta provider support (#11)
Browse files Browse the repository at this point in the history
* Removed debug

* Removed debug

* Added invalidation route

* Inital roll out of added functionality for vanta

* Undoing local dev changes

* Totally minor doc update

* Minor tweak to (hopefully) keep state checking stable until replaced

* One more tidy-up

* Another unchecked split

* Cleanup more stale debug comments

* Sloppy cleanup is sloppy

* allow oauth config to specify params to forward on auth request

* move vanta-specific logic to its own package

* Fix botched conflict resolution

---------

Co-authored-by: btoews <[email protected]>
  • Loading branch information
mjbraun and btoews authored Oct 28, 2024
1 parent 822a18a commit 9083d61
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 6 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/fly-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/

name: Fly Deploy
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
concurrency: deploy-group # optional: ensure only one action runs at a time
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ curl \

### Refreshing access tokens

Some identity providers issue access tokens that expire quickly along with refresh tokens that can be used to fetch new access tokens. To fetch a new access token, send a request to `https://<ssokenizer-url>/<provider-name>/refresh` via tokenizer. Include the sealed token in the `Proxy-Tokenizer` header, including a `st=refresh` parameter in the header. The response body will contain the new token, sealed for use with tokenizer.
Some identity providers issue access tokens that expire quickly along with refresh tokens that can be used to fetch new access tokens. To fetch a new access token, send a request to `https://<ssokenizer-url>/<provider-name>/refresh` via tokenizer. Include the sealed token in the `Proxy-Tokenizer` header, including a `st=refresh` parameter in the header. The response body will contain the new token, sealed for use with tokenizer and the Cache-Control header will contain the seconds until the token expires.

The following demonstrates how you might refresh a token using cURL:

Expand Down
17 changes: 17 additions & 0 deletions cmd/ssokenizer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/superfly/ssokenizer"
"github.com/superfly/ssokenizer/oauth2"
"github.com/superfly/ssokenizer/vanta"
"github.com/superfly/tokenizer"
xoauth2 "golang.org/x/oauth2"
"golang.org/x/oauth2/amazon"
Expand Down Expand Up @@ -172,6 +173,21 @@ type IdentityProviderConfig struct {

func (c IdentityProviderConfig) providerConfig(name, returnURL string) (ssokenizer.ProviderConfig, error) {
switch c.Profile {
case "vanta":
return &vanta.Config{
Path: "/" + name,
Config: xoauth2.Config{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
Scopes: c.Scopes,
Endpoint: xoauth2.Endpoint{
AuthURL: "https://app.vanta.com/oauth/authorize",
TokenURL: "https://api.vanta.com/oauth/token",
AuthStyle: xoauth2.AuthStyleInParams,
},
},
ForwardParams: []string{"source_id"},
}, nil
case "oauth":
return &oauth2.Config{
Path: "/" + name,
Expand Down Expand Up @@ -246,6 +262,7 @@ func (c IdentityProviderConfig) providerConfig(name, returnURL string) (ssokeniz
Scopes: c.Scopes,
Endpoint: google.Endpoint,
},
ForwardParams: []string{"hd"},
}, nil
case "heroku":
return &oauth2.Config{
Expand Down
12 changes: 11 additions & 1 deletion etc/ssokenizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,14 @@ identity_providers:
client_secret: "$GITHUB_CLIENT_SECRET"
return_url: "$GITHUB_AUTH_RETURN_URL"
scopes:
- "$GITHUB_AUTH_SCOPES"
- "$GITHUB_AUTH_SCOPES"

vanta:
secret_auth:
bearer: "$PROXY_AUTH"
profile: vanta
client_id: "$VANTA_CLIENT_ID"
client_secret: "$VANTA_CLIENT_SECRET"
return_url: "$VANTA_RETURN_URL"
scopes:
- "$VANTA_AUTH_SCOPES"
14 changes: 11 additions & 3 deletions oauth2/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ type Config struct {
// Regexp of hosts that oauth tokens are allowed to be used with. There is
// no need to anchor regexes.
AllowedHostPattern string

// ForwardParams are the parameters that should be forwarded from the start
// request to the auth URL.
ForwardParams []string
}

var _ ssokenizer.ProviderConfig = Config{}
Expand Down Expand Up @@ -81,14 +85,18 @@ func (p *provider) handleStart(w http.ResponseWriter, r *http.Request) {
if tr == nil {
return
}
cfg := p.config(r)

opts := []oauth2.AuthCodeOption{oauth2.AccessTypeOffline}

if hd := r.URL.Query().Get("hd"); hd != "" {
opts = append(opts, oauth2.SetAuthURLParam("hd", hd))
for _, param := range cfg.ForwardParams {
if value := r.URL.Query().Get(param); value != "" {
opts = append(opts, oauth2.SetAuthURLParam(param, value))
}
}

http.Redirect(w, r, p.config(r).AuthCodeURL(tr.Nonce, opts...), http.StatusFound)
url := cfg.AuthCodeURL(tr.Nonce, opts...)
http.Redirect(w, r, url, http.StatusFound)
}

func (p *provider) handleCallback(w http.ResponseWriter, r *http.Request) {
Expand Down
7 changes: 6 additions & 1 deletion ssokenizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/sirupsen/logrus"
"github.com/superfly/tokenizer"
"golang.org/x/exp/maps"
)

type Server struct {
Expand Down Expand Up @@ -49,7 +50,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {

provider, ok := s.providers[providerName]
if !ok {
GetLog(r).WithField("status", http.StatusNotFound).Info()
GetLog(r).WithFields(logrus.Fields{
"status": http.StatusNotFound,
"providers": maps.Keys(s.providers),
}).Info()

w.WriteHeader(http.StatusNotFound)
return
}
Expand Down
110 changes: 110 additions & 0 deletions vanta/vanta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package vanta

import (
"bytes"
"encoding/json"
"net/http"
"strings"
"time"

"github.com/superfly/ssokenizer"
"github.com/superfly/ssokenizer/oauth2"
"github.com/superfly/tokenizer"
xoauth2 "golang.org/x/oauth2"
)

const (
invalidatePath = "/invalidate"
invalidateURL = "https://api.vanta.com/v1/oauth/token/suspend"
)

type Config oauth2.Config

func (c Config) Register(sealKey string, auth tokenizer.AuthConfig) (http.Handler, error) {
handler, err := oauth2.Config(c).Register(sealKey, auth)
if err != nil {
return nil, err
}

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.TrimSuffix(r.URL.Path, "/") != invalidatePath {
handler.ServeHTTP(w, r)
return
}

var (
ctx = r.Context()
log = ssokenizer.GetLog(r)
)

accessToken, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
if !ok {
log.WithField("status", http.StatusUnauthorized).
Info("invalidate: missing token")

w.WriteHeader(http.StatusUnauthorized)
return
}

tok, err := c.TokenSource(ctx, &xoauth2.Token{AccessToken: accessToken}).Token()
if err != nil {
log.WithField("status", http.StatusForbidden).
WithError(err).
Info("invalidate: failed to get token")

w.WriteHeader(http.StatusForbidden)
return
}

if typ := tok.Type(); typ != "Bearer" {
log.WithField("status", http.StatusForbidden).
WithField("type", typ).
WithError(err).
Info("invalidate: bad token type")

w.WriteHeader(http.StatusForbidden)
return
}

body, err := json.Marshal(map[string]string{
"token": tok.AccessToken,
"client_id": c.ClientID,
"client_secret": c.ClientSecret,
})
if err != nil {
log.WithField("status", http.StatusInternalServerError).
WithError(err).
Info("invalidate: marshal json")

w.WriteHeader(http.StatusInternalServerError)
return
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, invalidateURL, bytes.NewBuffer(body))
if err != nil {
log.WithField("status", http.StatusInternalServerError).
WithError(err).
Info("invalidate: make request")

w.WriteHeader(http.StatusInternalServerError)
return
}

req.Header.Set("Content-Type", "application/json")
client := http.Client{Timeout: 10 * time.Second}

resp, err := client.Do(req)
if err != nil {
log.WithField("status", http.StatusServiceUnavailable).
WithError(err).
Info("invalidate: send request")

w.WriteHeader(http.StatusServiceUnavailable)
return
}

log.WithField("status", resp.Status).
Info("invalidate: success")
}), nil

}

0 comments on commit 9083d61

Please sign in to comment.