diff --git a/cmd/login.go b/cmd/login.go index b4a1273..abe3452 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -85,6 +85,7 @@ var loginCmd = &cobra.Command{ os.Exit(1) } + // start the listener err := opaal.Login(&config, &client, provider) if err != nil { fmt.Printf("%v\n", err) diff --git a/internal/flows/client_credentials.go b/internal/flows/client_credentials.go index e5789c2..1a70199 100644 --- a/internal/flows/client_credentials.go +++ b/internal/flows/client_credentials.go @@ -8,33 +8,34 @@ import ( type ClientCredentialsFlowParams struct { State string `yaml:"state"` ResponseType string `yaml:"response-type"` + Client *oauth.Client } type ClientCredentialsFlowEndpoints struct { - Create string + Clients string Authorize 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 - _, err := client.CreateOAuthClient(eps.Create) + res, err := params.Client.CreateOAuthClient(eps.Clients, []oauth.GrantType{oauth.ClientCredentials}) 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 - _, err = client.AuthorizeOAuthClient(eps.Authorize) + res, err = params.Client.AuthorizeOAuthClient(eps.Authorize) 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 - res, err := client.PerformTokenGrant(eps.Token, "") + res, err = params.Client.PerformClientCredentialsTokenGrant(eps.Token) 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)) - return nil + return string(res), nil } diff --git a/internal/flows/jwt_bearer.go b/internal/flows/jwt_bearer.go index 174a8f7..f36effa 100644 --- a/internal/flows/jwt_bearer.go +++ b/internal/flows/jwt_bearer.go @@ -29,14 +29,14 @@ type JwtBearerFlowParams struct { KeyPath string } -type JwtBearerEndpoints struct { +type JwtBearerFlowEndpoints struct { TrustedIssuers string Token string Clients 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 var ( 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 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 { 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) } 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 { return "", fmt.Errorf("failed to register client: %v", err) } @@ -210,7 +210,7 @@ func NewJwtBearerFlow(eps JwtBearerEndpoints, params JwtBearerFlowParams) (strin if eps.Token != "" { 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.PerformJwtBearerTokenGrant(eps.Token, string(newJwt)) if err != nil { 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 } -func ForwardToken(eps JwtBearerEndpoints, params JwtBearerFlowParams) error { +func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error { var ( client = params.Client idToken = params.IdToken @@ -279,7 +279,7 @@ func ForwardToken(eps JwtBearerEndpoints, params JwtBearerFlowParams) error { if verbose { 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 { 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) } 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 { return fmt.Errorf("failed to register client: %v", err) } @@ -327,7 +327,7 @@ func ForwardToken(eps JwtBearerEndpoints, params JwtBearerFlowParams) error { if verbose { 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 { return fmt.Errorf("failed to fetch access token: %v", err) } diff --git a/internal/login.go b/internal/login.go index caa7427..2ab0b04 100644 --- a/internal/login.go +++ b/internal/login.go @@ -14,7 +14,15 @@ import ( func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider) error { 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 @@ -38,12 +46,13 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider ) var button = MakeButton(authorizationUrl, "Login with "+client.Name) - var jwtClient = oauth.NewClient() - jwtClient.Scope = config.Authorization.Token.Scope + var authzClient = oauth.NewClient() + authzClient.Scope = config.Authorization.Token.Scope // authorize oauth client and listen for callback from provider fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", s.GetListenAddr()) params := server.ServerParams{ + Verbose: config.Options.Verbose, AuthProvider: &oidc.IdentityProvider{ Issuer: config.Authorization.Endpoints.Issuer, Endpoints: oidc.Endpoints{ @@ -51,14 +60,13 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider JwksUri: config.Authorization.Endpoints.JwksUri, }, }, - Verbose: config.Options.Verbose, - JwtBearerEndpoints: flows.JwtBearerEndpoints{ + JwtBearerEndpoints: flows.JwtBearerFlowEndpoints{ Token: config.Authorization.Endpoints.Token, TrustedIssuers: config.Authorization.Endpoints.TrustedIssuers, Register: config.Authorization.Endpoints.Register, }, JwtBearerParams: flows.JwtBearerFlowParams{ - Client: jwtClient, + Client: authzClient, IdentityProvider: provider, TrustedIssuer: &oauth.TrustedIssuer{ AllowAnySubject: false, @@ -70,8 +78,16 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider Verbose: config.Options.Verbose, 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) { fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n") } 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" { - err := NewClientCredentialsFlowWithConfig(config, client) + params := flows.ClientCredentialsFlowParams{ + Client: client, + } + _, err := NewClientCredentialsFlowWithConfig(config, params) if err != nil { fmt.Printf("failed to complete client credentials flow: %v", err) } diff --git a/internal/new.go b/internal/new.go index 0c2d3aa..7447ad1 100644 --- a/internal/new.go +++ b/internal/new.go @@ -72,13 +72,13 @@ func NewClientWithConfigById(config *Config, id string) *oauth.Client { return nil } -func NewClientCredentialsFlowWithConfig(config *Config, client *oauth.Client) error { +func NewClientCredentialsFlowWithConfig(config *Config, params flows.ClientCredentialsFlowParams) (string, error) { eps := flows.ClientCredentialsFlowEndpoints{ - Create: config.Authorization.Endpoints.Clients, + Clients: config.Authorization.Endpoints.Clients, Authorize: config.Authorization.Endpoints.Authorize, Token: config.Authorization.Endpoints.Token, } - return flows.NewClientCredentialsFlow(eps, client) + return flows.NewClientCredentialsFlow(eps, params) } func NewServerWithConfig(conf *Config) *server.Server { diff --git a/internal/oauth/client.go b/internal/oauth/client.go index 5110d49..2040117 100644 --- a/internal/oauth/client.go +++ b/internal/oauth/client.go @@ -14,6 +14,14 @@ import ( "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 { http.Client Id string `db:"id" yaml:"id"` @@ -87,21 +95,25 @@ func (client *Client) GetOAuthClient(clientUrl string) error { return nil } -func (client *Client) CreateOAuthClient(registerUrl string) ([]byte, error) { +func (client *Client) CreateOAuthClient(registerUrl string, grantTypes []GrantType) ([]byte, error) { // hydra endpoint: POST /clients + if client == nil { + return nil, fmt.Errorf("invalid client") + } audience := util.QuoteArrayStrings(client.Audience) + grantTypes = util.QuoteArrayStrings(grantTypes) body := httpx.Body(fmt.Sprintf(`{ "client_id": "%s", "client_name": "%s", "client_secret": "%s", "token_endpoint_auth_method": "client_secret_post", "scope": "%s", - "grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"], + "grant_types": [%s], "response_types": ["token"], "redirect_uris": ["http://127.0.0.1:3333/callback"], "state": 12345678910, "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{ "Content-Type": "application/json", @@ -131,22 +143,23 @@ func (client *Client) CreateOAuthClient(registerUrl string) ([]byte, error) { 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 if registerUrl == "" { return nil, fmt.Errorf("no URL provided") } audience := util.QuoteArrayStrings(client.Audience) + grantTypes = util.QuoteArrayStrings(grantTypes) body := httpx.Body(fmt.Sprintf(`{ "client_name": "opaal", "token_endpoint_auth_method": "client_secret_post", "scope": "%s", - "grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"], + "grant_types": [%s], "response_types": ["token"], "redirect_uris": ["http://127.0.0.1:3333/callback"], "state": 12345678910, "audience": [%s] - }`, strings.Join(client.Scope, " "), strings.Join(audience, ","), + }`, strings.Join(client.Scope, " "), strings.Join(grantTypes, ","), strings.Join(audience, ","), )) headers := httpx.Headers{ "Content-Type": "application/json", @@ -196,7 +209,7 @@ func (client *Client) AuthorizeOAuthClient(authorizeUrl string) ([]byte, error) 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 body := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + "&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 - client.FlowId = "" + return b, err +} + +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 } diff --git a/internal/server/server.go b/internal/server/server.go index e98f863..7739a67 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -24,10 +24,12 @@ type Server struct { } type ServerParams struct { - AuthProvider *oidc.IdentityProvider - Verbose bool - JwtBearerEndpoints flows.JwtBearerEndpoints - JwtBearerParams flows.JwtBearerFlowParams + AuthProvider *oidc.IdentityProvider + Verbose bool + ClientCredentialsEndpoints flows.ClientCredentialsFlowEndpoints + ClientCredentialsParams flows.ClientCredentialsFlowParams + JwtBearerEndpoints flows.JwtBearerFlowEndpoints + JwtBearerParams flows.JwtBearerFlowParams } func (s *Server) SetListenAddr(host string, port int) { @@ -38,7 +40,7 @@ func (s *Server) GetListenAddr() string { 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 = "" // check if callback is set @@ -114,30 +116,37 @@ func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client * 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 refreshToken := r.URL.Query().Get("refresh-token") - 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) - if err != nil { - fmt.Printf("failed to perform refresh token grant: %v\n", err) - http.Redirect(w, r, "/error", http.StatusInternalServerError) - return - } + if refreshToken != "" { + _, err := params.JwtBearerParams.Client.PerformRefreshTokenGrant(provider.Endpoints.Token, refreshToken) + if err != nil { + fmt.Printf("failed to perform refresh token grant: %v\n", err) + http.Redirect(w, r, "/error", http.StatusInternalServerError) + return + } - // return token to target if set or the sending client - returnTarget := r.URL.Query().Get("target") - if returnTarget == "" { - returnTarget = r.URL.Host - } - _, _, err = httpx.MakeHttpRequest(returnTarget, http.MethodPost, httpx.Body{}, httpx.Headers{}) - if err != nil { - fmt.Printf("failed to make request") - http.Redirect(w, r, "/error", http.StatusInternalServerError) + // return token to target if set or the sending client + returnTarget := r.URL.Query().Get("target") + if returnTarget == "" { + returnTarget = r.URL.Host + } + _, _, err = httpx.MakeHttpRequest(returnTarget, http.MethodPost, httpx.Body{}, httpx.Headers{}) + if err != nil { + fmt.Printf("failed to make request") + 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) { @@ -196,6 +205,13 @@ func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client * if params.Verbose { 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") if err != nil { panic(err)