Made changes to get client credentials grant working

This commit is contained in:
David J. Allen 2024-03-20 16:52:08 -06:00
parent e67bc3e010
commit 5173701fa0
No known key found for this signature in database
GPG key ID: 717C593FF60A2ACC
7 changed files with 135 additions and 64 deletions

View file

@ -85,6 +85,7 @@ var loginCmd = &cobra.Command{
os.Exit(1) os.Exit(1)
} }
// start the listener
err := opaal.Login(&config, &client, provider) err := opaal.Login(&config, &client, provider)
if err != nil { if err != nil {
fmt.Printf("%v\n", err) fmt.Printf("%v\n", err)

View file

@ -8,33 +8,34 @@ import (
type ClientCredentialsFlowParams struct { type ClientCredentialsFlowParams struct {
State string `yaml:"state"` State string `yaml:"state"`
ResponseType string `yaml:"response-type"` ResponseType string `yaml:"response-type"`
Client *oauth.Client
} }
type ClientCredentialsFlowEndpoints struct { type ClientCredentialsFlowEndpoints struct {
Create string Clients string
Authorize string Authorize string
Token string Token string
} }
func NewClientCredentialsFlow(eps ClientCredentialsFlowEndpoints, client *oauth.Client) error { func NewClientCredentialsFlow(eps ClientCredentialsFlowEndpoints, params ClientCredentialsFlowParams) (string, error) {
// register a new OAuth 2 client with authorization srever // register a new OAuth 2 client with authorization srever
_, err := client.CreateOAuthClient(eps.Create) res, err := params.Client.CreateOAuthClient(eps.Clients, []oauth.GrantType{oauth.ClientCredentials})
if err != nil { if err != nil {
return fmt.Errorf("failed to register OAuth client: %v", err) return "", fmt.Errorf("failed to register OAuth client: %v", err)
} }
// authorize the client // authorize the client
_, err = client.AuthorizeOAuthClient(eps.Authorize) res, err = params.Client.AuthorizeOAuthClient(eps.Authorize)
if err != nil { if err != nil {
return fmt.Errorf("failed to authorize client: %v", err) return "", fmt.Errorf("failed to authorize client: %v", err)
} }
// request a token from the authorization server // request a token from the authorization server
res, err := client.PerformTokenGrant(eps.Token, "") res, err = params.Client.PerformClientCredentialsTokenGrant(eps.Token)
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch token from authorization server: %v", err) return "", fmt.Errorf("failed to fetch token from authorization server: %v", err)
} }
fmt.Printf("token: %v\n", string(res)) fmt.Printf("token: %v\n", string(res))
return nil return string(res), nil
} }

View file

@ -29,14 +29,14 @@ type JwtBearerFlowParams struct {
KeyPath string KeyPath string
} }
type JwtBearerEndpoints struct { type JwtBearerFlowEndpoints struct {
TrustedIssuers string TrustedIssuers string
Token string Token string
Clients string Clients string
Register string Register string
} }
func NewJwtBearerFlow(eps JwtBearerEndpoints, params JwtBearerFlowParams) (string, error) { func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (string, error) {
// 1. verify that the JWT from the issuer is valid using all keys // 1. verify that the JWT from the issuer is valid using all keys
var ( var (
idp = params.IdentityProvider idp = params.IdentityProvider
@ -164,7 +164,7 @@ func NewJwtBearerFlow(eps JwtBearerEndpoints, params JwtBearerFlowParams) (strin
// 5. dynamically register new OAuth client and authorize it to make jwt_bearer request // 5. dynamically register new OAuth client and authorize it to make jwt_bearer request
fmt.Printf("Registering new OAuth2 client with authorization server...\n") fmt.Printf("Registering new OAuth2 client with authorization server...\n")
res, err = client.RegisterOAuthClient(eps.Register) res, err = client.RegisterOAuthClient(eps.Register, []oauth.GrantType{oauth.JwtBearer})
if err != nil { if err != nil {
return "", fmt.Errorf("failed to register client: %v", err) return "", fmt.Errorf("failed to register client: %v", err)
} }
@ -189,7 +189,7 @@ func NewJwtBearerFlow(eps JwtBearerEndpoints, params JwtBearerFlowParams) (strin
return "", fmt.Errorf("failed to delete OAuth client: %v", err) return "", fmt.Errorf("failed to delete OAuth client: %v", err)
} }
fmt.Printf("Attempting to re-create client...\n") fmt.Printf("Attempting to re-create client...\n")
res, err := client.CreateOAuthClient(eps.Clients) res, err := client.CreateOAuthClient(eps.Clients, []oauth.GrantType{oauth.JwtBearer})
if err != nil { if err != nil {
return "", fmt.Errorf("failed to register client: %v", err) return "", fmt.Errorf("failed to register client: %v", err)
} }
@ -210,7 +210,7 @@ func NewJwtBearerFlow(eps JwtBearerEndpoints, params JwtBearerFlowParams) (strin
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)) fmt.Printf("jwt: %s\n", string(newJwt))
res, err := client.PerformTokenGrant(eps.Token, string(newJwt)) res, err := client.PerformJwtBearerTokenGrant(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)
} }
@ -237,7 +237,7 @@ func NewJwtBearerFlow(eps JwtBearerEndpoints, params JwtBearerFlowParams) (strin
return string(res), nil return string(res), nil
} }
func ForwardToken(eps JwtBearerEndpoints, params JwtBearerFlowParams) error { func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error {
var ( var (
client = params.Client client = params.Client
idToken = params.IdToken idToken = params.IdToken
@ -279,7 +279,7 @@ func ForwardToken(eps JwtBearerEndpoints, params JwtBearerFlowParams) error {
if verbose { if verbose {
fmt.Printf("Registering new OAuth2 client with authorization server...\n") fmt.Printf("Registering new OAuth2 client with authorization server...\n")
} }
res, err := client.RegisterOAuthClient(eps.Register) res, err := client.RegisterOAuthClient(eps.Register, []oauth.GrantType{oauth.JwtBearer})
if err != nil { if err != nil {
return fmt.Errorf("failed to register client: %v", err) return fmt.Errorf("failed to register client: %v", err)
} }
@ -306,7 +306,7 @@ func ForwardToken(eps JwtBearerEndpoints, params JwtBearerFlowParams) error {
return fmt.Errorf("failed to delete OAuth client: %v", err) return fmt.Errorf("failed to delete OAuth client: %v", err)
} }
fmt.Printf("Attempting to re-create client...\n") fmt.Printf("Attempting to re-create client...\n")
res, err := client.CreateOAuthClient(eps.Clients) res, err := client.CreateOAuthClient(eps.Clients, []oauth.GrantType{oauth.JwtBearer})
if err != nil { if err != nil {
return fmt.Errorf("failed to register client: %v", err) return fmt.Errorf("failed to register client: %v", err)
} }
@ -327,7 +327,7 @@ func ForwardToken(eps JwtBearerEndpoints, params JwtBearerFlowParams) error {
if verbose { if verbose {
fmt.Printf("Fetching access token from authorization server...\n") fmt.Printf("Fetching access token from authorization server...\n")
} }
res, err := client.PerformTokenGrant(eps.Token, idToken) res, err := client.PerformJwtBearerTokenGrant(eps.Token, idToken)
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

@ -14,7 +14,15 @@ import (
func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider) error { func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider) error {
if config == nil { if config == nil {
return fmt.Errorf("config is not valid") return fmt.Errorf("invalid config")
}
if client == nil {
return fmt.Errorf("invalid client")
}
if provider == nil {
return fmt.Errorf("invalid identity provider")
} }
// make cache if it's not where expect // make cache if it's not where expect
@ -38,12 +46,13 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
) )
var button = MakeButton(authorizationUrl, "Login with "+client.Name) var button = MakeButton(authorizationUrl, "Login with "+client.Name)
var jwtClient = oauth.NewClient() var authzClient = oauth.NewClient()
jwtClient.Scope = config.Authorization.Token.Scope authzClient.Scope = config.Authorization.Token.Scope
// 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", s.GetListenAddr()) fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", s.GetListenAddr())
params := server.ServerParams{ params := server.ServerParams{
Verbose: config.Options.Verbose,
AuthProvider: &oidc.IdentityProvider{ AuthProvider: &oidc.IdentityProvider{
Issuer: config.Authorization.Endpoints.Issuer, Issuer: config.Authorization.Endpoints.Issuer,
Endpoints: oidc.Endpoints{ Endpoints: oidc.Endpoints{
@ -51,14 +60,13 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
JwksUri: config.Authorization.Endpoints.JwksUri, JwksUri: config.Authorization.Endpoints.JwksUri,
}, },
}, },
Verbose: config.Options.Verbose, JwtBearerEndpoints: flows.JwtBearerFlowEndpoints{
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,
}, },
JwtBearerParams: flows.JwtBearerFlowParams{ JwtBearerParams: flows.JwtBearerFlowParams{
Client: jwtClient, Client: authzClient,
IdentityProvider: provider, IdentityProvider: provider,
TrustedIssuer: &oauth.TrustedIssuer{ TrustedIssuer: &oauth.TrustedIssuer{
AllowAnySubject: false, AllowAnySubject: false,
@ -70,8 +78,16 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
Verbose: config.Options.Verbose, Verbose: config.Options.Verbose,
Refresh: config.Authorization.Token.Refresh, Refresh: config.Authorization.Token.Refresh,
}, },
ClientCredentialsEndpoints: flows.ClientCredentialsFlowEndpoints{
Clients: config.Authorization.Endpoints.Clients,
Authorize: config.Authorization.Endpoints.Authorize,
Token: config.Authorization.Endpoints.Token,
},
ClientCredentialsParams: flows.ClientCredentialsFlowParams{
Client: authzClient,
},
} }
err = s.Login(button, provider, client, params) err = s.Start(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 {
@ -79,7 +95,10 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
} }
} else if config.Options.FlowType == "client_credentials" { } else if config.Options.FlowType == "client_credentials" {
err := NewClientCredentialsFlowWithConfig(config, client) params := flows.ClientCredentialsFlowParams{
Client: client,
}
_, err := NewClientCredentialsFlowWithConfig(config, params)
if err != nil { if err != nil {
fmt.Printf("failed to complete client credentials flow: %v", err) fmt.Printf("failed to complete client credentials flow: %v", err)
} }

View file

@ -72,13 +72,13 @@ func NewClientWithConfigById(config *Config, id string) *oauth.Client {
return nil return nil
} }
func NewClientCredentialsFlowWithConfig(config *Config, client *oauth.Client) error { func NewClientCredentialsFlowWithConfig(config *Config, params flows.ClientCredentialsFlowParams) (string, error) {
eps := flows.ClientCredentialsFlowEndpoints{ eps := flows.ClientCredentialsFlowEndpoints{
Create: config.Authorization.Endpoints.Clients, Clients: config.Authorization.Endpoints.Clients,
Authorize: config.Authorization.Endpoints.Authorize, Authorize: config.Authorization.Endpoints.Authorize,
Token: config.Authorization.Endpoints.Token, Token: config.Authorization.Endpoints.Token,
} }
return flows.NewClientCredentialsFlow(eps, client) return flows.NewClientCredentialsFlow(eps, params)
} }
func NewServerWithConfig(conf *Config) *server.Server { func NewServerWithConfig(conf *Config) *server.Server {

View file

@ -14,6 +14,14 @@ import (
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
) )
type GrantType = string
const (
AuthorizationCode GrantType = "authorization_code"
ClientCredentials GrantType = "client_credentials"
JwtBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
)
type Client struct { type Client struct {
http.Client http.Client
Id string `db:"id" yaml:"id"` Id string `db:"id" yaml:"id"`
@ -87,21 +95,25 @@ func (client *Client) GetOAuthClient(clientUrl string) error {
return nil return nil
} }
func (client *Client) CreateOAuthClient(registerUrl string) ([]byte, error) { func (client *Client) CreateOAuthClient(registerUrl string, grantTypes []GrantType) ([]byte, error) {
// hydra endpoint: POST /clients // hydra endpoint: POST /clients
if client == nil {
return nil, fmt.Errorf("invalid client")
}
audience := util.QuoteArrayStrings(client.Audience) audience := util.QuoteArrayStrings(client.Audience)
grantTypes = util.QuoteArrayStrings(grantTypes)
body := httpx.Body(fmt.Sprintf(`{ body := httpx.Body(fmt.Sprintf(`{
"client_id": "%s", "client_id": "%s",
"client_name": "%s", "client_name": "%s",
"client_secret": "%s", "client_secret": "%s",
"token_endpoint_auth_method": "client_secret_post", "token_endpoint_auth_method": "client_secret_post",
"scope": "%s", "scope": "%s",
"grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"], "grant_types": [%s],
"response_types": ["token"], "response_types": ["token"],
"redirect_uris": ["http://127.0.0.1:3333/callback"], "redirect_uris": ["http://127.0.0.1:3333/callback"],
"state": 12345678910, "state": 12345678910,
"audience": [%s] "audience": [%s]
}`, client.Id, client.Id, client.Secret, strings.Join(client.Scope, " "), strings.Join(audience, ","), }`, client.Id, client.Id, client.Secret, strings.Join(client.Scope, " "), strings.Join(grantTypes, ","), strings.Join(audience, ","),
)) ))
headers := httpx.Headers{ headers := httpx.Headers{
"Content-Type": "application/json", "Content-Type": "application/json",
@ -131,22 +143,23 @@ func (client *Client) CreateOAuthClient(registerUrl string) ([]byte, error) {
return b, err return b, err
} }
func (client *Client) RegisterOAuthClient(registerUrl string) ([]byte, error) { func (client *Client) RegisterOAuthClient(registerUrl string, grantTypes []GrantType) ([]byte, error) {
// hydra endpoint: POST /oauth2/register // hydra endpoint: POST /oauth2/register
if registerUrl == "" { if registerUrl == "" {
return nil, fmt.Errorf("no URL provided") return nil, fmt.Errorf("no URL provided")
} }
audience := util.QuoteArrayStrings(client.Audience) audience := util.QuoteArrayStrings(client.Audience)
grantTypes = util.QuoteArrayStrings(grantTypes)
body := httpx.Body(fmt.Sprintf(`{ body := httpx.Body(fmt.Sprintf(`{
"client_name": "opaal", "client_name": "opaal",
"token_endpoint_auth_method": "client_secret_post", "token_endpoint_auth_method": "client_secret_post",
"scope": "%s", "scope": "%s",
"grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"], "grant_types": [%s],
"response_types": ["token"], "response_types": ["token"],
"redirect_uris": ["http://127.0.0.1:3333/callback"], "redirect_uris": ["http://127.0.0.1:3333/callback"],
"state": 12345678910, "state": 12345678910,
"audience": [%s] "audience": [%s]
}`, strings.Join(client.Scope, " "), strings.Join(audience, ","), }`, strings.Join(client.Scope, " "), strings.Join(grantTypes, ","), strings.Join(audience, ","),
)) ))
headers := httpx.Headers{ headers := httpx.Headers{
"Content-Type": "application/json", "Content-Type": "application/json",
@ -196,7 +209,7 @@ func (client *Client) AuthorizeOAuthClient(authorizeUrl string) ([]byte, error)
return b, nil return b, nil
} }
func (client *Client) PerformTokenGrant(clientUrl string, encodedJwt string) ([]byte, error) { func (client *Client) PerformJwtBearerTokenGrant(clientUrl string, encodedJwt string) ([]byte, error) {
// hydra endpoint: /oauth/token // hydra endpoint: /oauth/token
body := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + body := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") +
"&client_id=" + client.Id + "&client_id=" + client.Id +
@ -220,8 +233,29 @@ func (client *Client) PerformTokenGrant(clientUrl string, encodedJwt string) ([]
} }
// set flow ID back to empty string to indicate a completed flow return b, err
client.FlowId = "" }
func (client *Client) PerformClientCredentialsTokenGrant(clientUrl string) ([]byte, error) {
// hydra endpoint: /oauth/token
body := "grant_type=" + url.QueryEscape("client_credentials") +
"&client_id=" + client.Id +
"&client_secret=" + client.Secret +
"&redirect_uri=" + url.QueryEscape("http://127.0.0.1:3333/callback")
// add optional params if valid
if client.Scope != nil || len(client.Scope) > 0 {
body += "&scope=" + strings.Join(client.Scope, "+")
}
headers := httpx.Headers{
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Bearer " + client.RegistrationAccessToken,
}
_, b, err := httpx.MakeHttpRequest(clientUrl, http.MethodPost, []byte(body), headers)
if err != nil {
return nil, fmt.Errorf("failed to make HTTP request: %v", err)
}
return b, err return b, err
} }

View file

@ -26,7 +26,9 @@ type Server struct {
type ServerParams struct { type ServerParams struct {
AuthProvider *oidc.IdentityProvider AuthProvider *oidc.IdentityProvider
Verbose bool Verbose bool
JwtBearerEndpoints flows.JwtBearerEndpoints ClientCredentialsEndpoints flows.ClientCredentialsFlowEndpoints
ClientCredentialsParams flows.ClientCredentialsFlowParams
JwtBearerEndpoints flows.JwtBearerFlowEndpoints
JwtBearerParams flows.JwtBearerFlowParams JwtBearerParams flows.JwtBearerFlowParams
} }
@ -38,7 +40,7 @@ func (s *Server) GetListenAddr() string {
return fmt.Sprintf("%s:%d", s.Host, s.Port) return fmt.Sprintf("%s:%d", s.Host, s.Port)
} }
func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client *oauth.Client, params ServerParams) error { func (s *Server) Start(buttons string, provider *oidc.IdentityProvider, client *oauth.Client, params ServerParams) error {
var target = "" var target = ""
// check if callback is set // check if callback is set
@ -114,14 +116,10 @@ func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client *
w.Write(jwks) w.Write(jwks)
}) })
r.HandleFunc("/refresh", func(w http.ResponseWriter, r *http.Request) { r.HandleFunc("/token", 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 == "" { if refreshToken != "" {
fmt.Printf("no refresh token provided")
http.Redirect(w, r, "/error", http.StatusBadRequest)
return
}
_, err := params.JwtBearerParams.Client.PerformRefreshTokenGrant(provider.Endpoints.Token, refreshToken) _, err := params.JwtBearerParams.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)
@ -138,6 +136,17 @@ func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client *
if err != nil { if err != nil {
fmt.Printf("failed to make request") fmt.Printf("failed to make request")
http.Redirect(w, r, "/error", http.StatusInternalServerError) http.Redirect(w, r, "/error", http.StatusInternalServerError)
return
}
} else {
// perform a client credentials grant and return a token
var err error
accessToken, err = flows.NewClientCredentialsFlow(params.ClientCredentialsEndpoints, params.ClientCredentialsParams)
if err != nil {
fmt.Printf("failed to perform client credentials flow: %v\n", err)
http.Redirect(w, r, "/error", http.StatusInternalServerError)
return
}
} }
}) })
r.HandleFunc(s.Callback, func(w http.ResponseWriter, r *http.Request) { r.HandleFunc(s.Callback, func(w http.ResponseWriter, r *http.Request) {
@ -196,6 +205,13 @@ func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client *
if params.Verbose { if params.Verbose {
fmt.Printf("Serving success page.\n") fmt.Printf("Serving success page.\n")
} }
// return only the token with no web page if "no-browser" header is set
noBrowser := r.Header.Get("no-browser")
if noBrowser != "" {
return
}
template, err := gonja.FromFile("pages/success.html") template, err := gonja.FromFile("pages/success.html")
if err != nil { if err != nil {
panic(err) panic(err)