Added token refresh flag

This commit is contained in:
David Allen 2024-03-19 11:21:05 -06:00
parent aecfa30e2c
commit d0f8c9087d
No known key found for this signature in database
GPG key ID: 1D2A29322FBB6FCB
5 changed files with 44 additions and 18 deletions

View file

@ -24,7 +24,6 @@ type Options struct {
FlowType string `yaml:"flow"` FlowType string `yaml:"flow"`
CachePath string `yaml:"cache"` CachePath string `yaml:"cache"`
CacheOnly bool `yaml:"cache-only"` CacheOnly bool `yaml:"cache-only"`
Refresh bool `yaml:"refresh"`
Verbose bool `yaml:"verbose"` Verbose bool `yaml:"verbose"`
} }
@ -53,6 +52,7 @@ type Authorization struct {
KeyPath string `yaml:"key-path"` KeyPath string `yaml:"key-path"`
TokenDuration time.Duration `yaml:"token-duration"` TokenDuration time.Duration `yaml:"token-duration"`
TokenForwarding bool `yaml:"token-forwarding"` TokenForwarding bool `yaml:"token-forwarding"`
TokenRefresh bool `yaml:"refresh"`
} }
type Config struct { type Config struct {
@ -77,7 +77,6 @@ func NewConfig() Config {
CachePath: "opaal.db", CachePath: "opaal.db",
FlowType: "authorization_code", FlowType: "authorization_code",
CacheOnly: false, CacheOnly: false,
Refresh: true,
Verbose: false, Verbose: false,
}, },
Authentication: Authentication{ Authentication: Authentication{
@ -87,6 +86,7 @@ func NewConfig() Config {
KeyPath: "./keys", KeyPath: "./keys",
TokenForwarding: false, TokenForwarding: false,
TokenDuration: 1 * time.Hour, TokenDuration: 1 * time.Hour,
TokenRefresh: true,
}, },
} }
} }

View file

@ -85,7 +85,7 @@ func NewJwtBearerFlow(eps JwtBearerEndpoints, params JwtBearerFlowParams) (strin
b := cryptox.MarshalRSAPrivateKey(privateKey) b := cryptox.MarshalRSAPrivateKey(privateKey)
err = os.WriteFile(keyPath, b, os.ModePerm) err = os.WriteFile(keyPath, b, os.ModePerm)
if err != nil { if err != nil {
fmt.Printf("failed to write private key to file: %v", err) fmt.Printf("failed to write private key to file: %v\n", err)
} }
} else { } else {
privateKey, err := cryptox.GenerateRSAPrivateKey(rawPrivateKey) privateKey, err := cryptox.GenerateRSAPrivateKey(rawPrivateKey)
@ -140,9 +140,18 @@ func NewJwtBearerFlow(eps JwtBearerEndpoints, params JwtBearerFlowParams) (strin
// include the offline_access scope if refresh tokens are enabled // include the offline_access scope if refresh tokens are enabled
if params.Refresh { if params.Refresh {
scope := payload["scp"].([]string) v, ok := payload["scope"]
scope = append(scope, "offline_access") if !ok {
payload["scp"] = scope payload["scope"] = []string{"offline_access"}
} else {
// FIXME: probably should not assume scope is []string even though it should be
scope := v.([]string)
scope = append(scope, "offline_access")
payload["scope"] = scope
}
// also include offline_access in client to make request
client.Scope = append(client.Scope, "offline_access")
} }
payloadJson, err := json.Marshal(payload) payloadJson, err := json.Marshal(payload)
if err != nil { if err != nil {
@ -201,6 +210,7 @@ func NewJwtBearerFlow(eps JwtBearerEndpoints, params JwtBearerFlowParams) (strin
// 6. send JWT to authorization server and receive a access token // 6. send JWT to authorization server and receive a access token
if eps.Token != "" { if eps.Token != "" {
fmt.Printf("Fetching access token from authorization server...\n") fmt.Printf("Fetching access token from authorization server...\n")
fmt.Printf("jwt: %s\n", string(newJwt))
res, err := client.PerformTokenGrant(eps.Token, string(newJwt)) res, err := client.PerformTokenGrant(eps.Token, string(newJwt))
if err != nil { if err != nil {
return "", fmt.Errorf("failed to fetch access token: %v", err) return "", fmt.Errorf("failed to fetch access token: %v", err)

View file

@ -5,6 +5,7 @@ import (
"davidallendj/opaal/internal/flows" "davidallendj/opaal/internal/flows"
"davidallendj/opaal/internal/oauth" "davidallendj/opaal/internal/oauth"
"davidallendj/opaal/internal/oidc" "davidallendj/opaal/internal/oidc"
"davidallendj/opaal/internal/server"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -31,15 +32,15 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
// print the authorization URL for sharing // print the authorization URL for sharing
var authorizationUrl = client.BuildAuthorizationUrl(provider.Endpoints.Authorization, state) var authorizationUrl = client.BuildAuthorizationUrl(provider.Endpoints.Authorization, state)
server := NewServerWithConfig(config) s := NewServerWithConfig(config)
fmt.Printf("Login with identity provider:\n\n %s/login\n %s\n\n", fmt.Printf("Login with identity provider:\n\n %s/login\n %s\n\n",
server.GetListenAddr(), authorizationUrl, s.GetListenAddr(), authorizationUrl,
) )
var button = MakeButton(authorizationUrl, "Login with "+client.Name) var button = MakeButton(authorizationUrl, "Login with "+client.Name)
// authorize oauth client and listen for callback from provider // authorize oauth client and listen for callback from provider
fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", server.GetListenAddr()) fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", s.GetListenAddr())
params := server.ServerParams{ params := server.ServerParams{
AuthProvider: &oidc.IdentityProvider{ AuthProvider: &oidc.IdentityProvider{
Issuer: config.Authorization.Endpoints.Issuer, Issuer: config.Authorization.Endpoints.Issuer,
@ -49,24 +50,26 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
}, },
}, },
Verbose: config.Options.Verbose, Verbose: config.Options.Verbose,
JwtBearerFlowEndpoints: flows.JwtBearerEndpoints{ JwtBearerEndpoints: flows.JwtBearerEndpoints{
Token: config.Authorization.Endpoints.Token, Token: config.Authorization.Endpoints.Token,
TrustedIssuers: config.Authorization.Endpoints.TrustedIssuers, TrustedIssuers: config.Authorization.Endpoints.TrustedIssuers,
Register: config.Authorization.Endpoints.Register, Register: config.Authorization.Endpoints.Register,
}, },
JwtBearerFlowParams: flows.JwtBearerFlowParams{ JwtBearerParams: flows.JwtBearerFlowParams{
Client: oauth.NewClient(), Client: oauth.NewClient(),
IdentityProvider: provider, IdentityProvider: provider,
TrustedIssuer: &oauth.TrustedIssuer{ TrustedIssuer: &oauth.TrustedIssuer{
AllowAnySubject: false, AllowAnySubject: false,
Issuer: server.Addr, Issuer: s.Addr,
Subject: "opaal", Subject: "opaal",
ExpiresAt: time.Now().Add(config.Authorization.TokenDuration), ExpiresAt: time.Now().Add(config.Authorization.TokenDuration),
Scope: []string{},
}, },
Verbose: config.Options.Verbose, Verbose: config.Options.Verbose,
Refresh: config.Authorization.TokenRefresh,
}, },
} }
err = server.Login(button, provider, client, params) err = s.Login(button, provider, client, params)
if errors.Is(err, http.ErrServerClosed) { if errors.Is(err, http.ErrServerClosed) {
fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n") fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n")
} else if err != nil { } else if err != nil {

View file

@ -62,8 +62,12 @@ func (client *Client) ListTrustedIssuers(url string) ([]TrustedIssuer, error) {
func (client *Client) AddTrustedIssuer(url string, ti *TrustedIssuer) ([]byte, error) { func (client *Client) AddTrustedIssuer(url string, ti *TrustedIssuer) ([]byte, error) {
// hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers // hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers
quotedScopes := make([]string, len(client.Scope)) if ti == nil {
for i, s := range client.Scope { return nil, fmt.Errorf("no valid trusted issuer provided")
}
quotedScopes := make([]string, len(ti.Scope))
for i, s := range ti.Scope {
quotedScopes[i] = fmt.Sprintf("\"%s\"", s) quotedScopes[i] = fmt.Sprintf("\"%s\"", s)
} }
@ -73,13 +77,13 @@ func (client *Client) AddTrustedIssuer(url string, ti *TrustedIssuer) ([]byte, e
"issuer": ti.Issuer, "issuer": ti.Issuer,
"expires_at": ti.ExpiresAt, "expires_at": ti.ExpiresAt,
"jwk": ti.PublicKey, "jwk": ti.PublicKey,
"scope": client.Scope, "scope": ti.Scope,
} }
if !ti.AllowAnySubject { if !ti.AllowAnySubject {
body["subject"] = ti.Subject body["subject"] = ti.Subject
} }
b, err := json.Marshal(body) b, err := json.Marshal(body)
fmt.Printf("request: %v\n", string(b)) // fmt.Printf("request: %v\n", string(b))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %v", err) return nil, fmt.Errorf("failed to marshal request body: %v", err)
} }

View file

@ -93,10 +93,14 @@ func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client *
err := p.FetchServerConfig() err := p.FetchServerConfig()
if err != nil { if err != nil {
fmt.Printf("failed to fetch server config: %v\n", err) fmt.Printf("failed to fetch server config: %v\n", err)
http.Redirect(w, r, "/error", http.StatusInternalServerError)
return
} }
err = p.FetchJwks() err = p.FetchJwks()
if err != nil { if err != nil {
fmt.Printf("failed to fetch JWKS after fetching server config: %v\n", err) fmt.Printf("failed to fetch JWKS after fetching server config: %v\n", err)
http.Redirect(w, r, "/error", http.StatusInternalServerError)
return
} }
} }
@ -113,6 +117,11 @@ func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client *
r.HandleFunc("/refresh", func(w http.ResponseWriter, r *http.Request) { r.HandleFunc("/refresh", func(w http.ResponseWriter, r *http.Request) {
// use refresh token provided to do a refresh token grant // use refresh token provided to do a refresh token grant
refreshToken := r.URL.Query().Get("refresh-token") refreshToken := r.URL.Query().Get("refresh-token")
if refreshToken == "" {
fmt.Printf("no refresh token provided")
http.Redirect(w, r, "/error", http.StatusBadRequest)
return
}
_, err := client.PerformRefreshTokenGrant(provider.Endpoints.Token, refreshToken) _, err := client.PerformRefreshTokenGrant(provider.Endpoints.Token, refreshToken)
if err != nil { if err != nil {
fmt.Printf("failed to perform refresh token grant: %v\n", err) fmt.Printf("failed to perform refresh token grant: %v\n", err)
@ -214,7 +223,7 @@ func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client *
if params.Verbose { if params.Verbose {
fmt.Printf("Serving error page.") fmt.Printf("Serving error page.")
} }
template, err := gonja.FromFile("pages/success.html") template, err := gonja.FromFile("pages/error.html")
if err != nil { if err != nil {
panic(err) panic(err)
} }