Skip to content

Commit

Permalink
Merge pull request #15764 from hashicorp/jrasell/gh-13120-oidc-login
Browse files Browse the repository at this point in the history
sso: add OIDC login RPC, HTTP, and CLI workflow
  • Loading branch information
jrasell authored Jan 18, 2023
2 parents 4cd60c3 + d57b805 commit ca753cf
Show file tree
Hide file tree
Showing 31 changed files with 2,302 additions and 82 deletions.
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

0 comments on commit ca753cf

Please sign in to comment.