Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sso: login workflow implementation #15816

Merged
merged 11 commits into from
Jan 19, 2023
3 changes: 3 additions & 0 deletions .semgrep/rpc_endpoint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ rules:
- pattern-not: '"ACL.ResolveToken"'
- pattern-not: '"ACL.UpsertOneTimeToken"'
- pattern-not: '"ACL.ExchangeOneTimeToken"'
- pattern-not: 'structs.ACLListAuthMethodsRPCMethod'
- pattern-not: 'structs.ACLOIDCAuthURLRPCMethod'
- pattern-not: 'structs.ACLOIDCCompleteAuthRPCMethod'
- pattern-not: '"CSIPlugin.Get"'
- pattern-not: '"CSIPlugin.List"'
- pattern-not: '"Status.Leader"'
Expand Down
80 changes: 80 additions & 0 deletions api/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,38 @@ func (a *ACLBindingRules) Get(bindingRuleID string, q *QueryOptions) (*ACLBindin
return &resp, qm, nil
}

// ACLOIDC is used to query the ACL OIDC endpoints.
type ACLOIDC struct {
client *Client
}

// ACLOIDC returns a new handle on the ACL auth-methods API client.
func (c *Client) ACLOIDC() *ACLOIDC {
return &ACLOIDC{client: c}
}

// GetAuthURL generates the OIDC provider authentication URL. This URL should
// be visited in order to sign in to the provider.
func (a *ACLOIDC) GetAuthURL(req *ACLOIDCAuthURLRequest, q *WriteOptions) (*ACLOIDCAuthURLResponse, *WriteMeta, error) {
var resp ACLOIDCAuthURLResponse
wm, err := a.client.write("/v1/acl/oidc/auth-url", req, &resp, q)
if err != nil {
return nil, nil, err
}
return &resp, wm, nil
}

// CompleteAuth exchanges the OIDC provider token for a Nomad token with the
// appropriate claims attached.
func (a *ACLOIDC) CompleteAuth(req *ACLOIDCCompleteAuthRequest, q *WriteOptions) (*ACLToken, *WriteMeta, error) {
var resp ACLToken
wm, err := a.client.write("/v1/acl/oidc/complete-auth", req, &resp, q)
if err != nil {
return nil, nil, err
}
return &resp, wm, nil
}

// ACLPolicyListStub is used to for listing ACL policies
type ACLPolicyListStub struct {
Name string
Expand Down Expand Up @@ -666,6 +698,7 @@ type ACLAuthMethodConfig struct {
OIDCDiscoveryURL string
OIDCClientID string
OIDCClientSecret string
OIDCScopes []string
BoundAudiences []string
AllowedRedirectURIs []string
DiscoveryCaPem []string
Expand Down Expand Up @@ -816,3 +849,50 @@ type ACLBindingRuleListStub struct {
CreateIndex uint64
ModifyIndex uint64
}

// ACLOIDCAuthURLRequest is the request to make when starting the OIDC
// authentication login flow.
type ACLOIDCAuthURLRequest struct {

// AuthMethodName is the OIDC auth-method to use. This is a required
// parameter.
AuthMethodName string

// RedirectURI is the URL that authorization should redirect to. This is a
// required parameter.
RedirectURI string

// ClientNonce is a randomly generated string to prevent replay attacks. It
// is up to the client to generate this and Go integrations should use the
// oidc.NewID function within the hashicorp/cap library.
ClientNonce string
}

// ACLOIDCAuthURLResponse is the response when starting the OIDC authentication
// login flow.
type ACLOIDCAuthURLResponse struct {

// AuthURL is URL to begin authorization and is where the user logging in
// should go.
AuthURL string
}

// ACLOIDCCompleteAuthRequest is the request object to begin completing the
// OIDC auth cycle after receiving the callback from the OIDC provider.
type ACLOIDCCompleteAuthRequest struct {

// AuthMethodName is the name of the auth method being used to login via
// OIDC. This will match AuthUrlArgs.AuthMethodName. This is a required
// parameter.
AuthMethodName string

// ClientNonce, State, and Code are provided from the parameters given to
// the redirect URL. These are all required parameters.
ClientNonce string
State string
Code string

// RedirectURI is the URL that authorization should redirect to. This is a
// required parameter.
RedirectURI string
}
1 change: 1 addition & 0 deletions command/acl_auth_method.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func formatAuthMethodConfig(config *api.ACLAuthMethodConfig) []string {
fmt.Sprintf("OIDC Discovery URL|%s", config.OIDCDiscoveryURL),
fmt.Sprintf("OIDC Client ID|%s", config.OIDCClientID),
fmt.Sprintf("OIDC Client Secret|%s", config.OIDCClientSecret),
fmt.Sprintf("OIDC Scopes|%s", strings.Join(config.OIDCScopes, ",")),
fmt.Sprintf("Bound audiences|%s", strings.Join(config.BoundAudiences, ",")),
fmt.Sprintf("Allowed redirects URIs|%s", strings.Join(config.AllowedRedirectURIs, ",")),
fmt.Sprintf("Discovery CA pem|%s", strings.Join(config.DiscoveryCaPem, ",")),
Expand Down
8 changes: 6 additions & 2 deletions command/acl_auth_method_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,19 @@ func TestACLAuthMethodListCommand(t *testing.T) {
ui := cli.NewMockUi()
cmd := &ACLAuthMethodListCommand{Meta: Meta{Ui: ui, flagAddress: url}}

// Attempt to list auth methods without a valid management token
// List with an invalid token works fine
invalidToken := mock.ACLToken()
code := cmd.Run([]string{"-address=" + url, "-token=" + invalidToken.SecretID})
must.One(t, code)
must.Zero(t, code)

// List with a valid management token
code = cmd.Run([]string{"-address=" + url, "-token=" + token.SecretID})
must.Zero(t, code)

// List with no token at all
code = cmd.Run([]string{"-address=" + url})
must.Zero(t, code)

// Check the output
out := ui.OutputWriter.String()
must.StrContains(t, out, method.Name)
Expand Down
45 changes: 45 additions & 0 deletions command/agent/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -829,3 +829,48 @@ func (s *HTTPServer) aclBindingRuleUpsertRequest(
}
return nil, nil
}

// ACLOIDCAuthURLRequest starts the OIDC login workflow.
func (s *HTTPServer) ACLOIDCAuthURLRequest(_ http.ResponseWriter, req *http.Request) (interface{}, error) {

// The endpoint only supports PUT or POST requests.
if req.Method != http.MethodPost && req.Method != http.MethodPut {
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
}

var args structs.ACLOIDCAuthURLRequest
s.parseWriteRequest(req, &args.WriteRequest)

if err := decodeBody(req, &args); err != nil {
return nil, CodedError(http.StatusBadRequest, err.Error())
}

var out structs.ACLOIDCAuthURLResponse
if err := s.agent.RPC(structs.ACLOIDCAuthURLRPCMethod, &args, &out); err != nil {
return nil, err
}
return out, nil
}

// ACLOIDCCompleteAuthRequest completes the OIDC login workflow.
func (s *HTTPServer) ACLOIDCCompleteAuthRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {

// The endpoint only supports PUT or POST requests.
if req.Method != http.MethodPost && req.Method != http.MethodPut {
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
}

var args structs.ACLOIDCCompleteAuthRequest
s.parseWriteRequest(req, &args.WriteRequest)

if err := decodeBody(req, &args); err != nil {
return nil, CodedError(http.StatusBadRequest, err.Error())
}

var out structs.ACLOIDCCompleteAuthResponse
if err := s.agent.RPC(structs.ACLOIDCCompleteAuthRPCMethod, &args, &out); err != nil {
return nil, err
}
setIndex(resp, out.Index)
return out.ACLToken, nil
}
Loading