Skip to content

Commit

Permalink
Adds LoginWithClientCredentials to Admin facade version 4.
Browse files Browse the repository at this point in the history
  • Loading branch information
alesstimec committed Mar 8, 2024
1 parent 0d3ec34 commit 2366cf9
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 0 deletions.
7 changes: 7 additions & 0 deletions api/params/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,13 @@ type LoginWithSessionTokenRequest struct {

// Service Account related request parameters

// LoginWithClientCredentialsRequest holds the client id and secret used
// to authenticate with JIMM.
type LoginWithClientCredentialsRequest struct {
ClientID string `json:"client-id"`
ClientSecret string `json:"client-secret"`
}

// AddServiceAccountRequest holds a request to add a service account.
type AddServiceAccountRequest struct {
// ClientID holds the client id of the service account.
Expand Down
19 changes: 19 additions & 0 deletions internal/auth/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/juju/zaputil/zapctx"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
gosdk_oauth2 "github.com/openfga/go-sdk/oauth2"
"github.com/openfga/go-sdk/oauth2/clientcredentials"
"go.uber.org/zap"
"golang.org/x/oauth2"

Expand Down Expand Up @@ -310,3 +312,20 @@ func VerifySessionToken(token string, secretKey string) (jwt.Token, error) {

return parsedToken, nil
}

// VerifyClientCredentials verifies the provided client ID and client secret.
func (as *AuthenticationService) VerifyClientCredentials(ctx context.Context, clientID string, clientSecret string) error {
cfg := clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: as.oauthConfig.Endpoint.TokenURL,
Scopes: as.oauthConfig.Scopes,
AuthStyle: gosdk_oauth2.AuthStyle(as.oauthConfig.Endpoint.AuthStyle),
}

_, err := cfg.Token(ctx)
if err != nil {
return errors.E(errors.CodeUnauthorized, "invalid client credentials")
}
return nil
}
18 changes: 18 additions & 0 deletions internal/auth/oauth2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,21 @@ func TestSessionTokenValidatesEmail(t *testing.T) {
_, err = authSvc.VerifySessionToken(token, secretKey)
c.Assert(err, qt.ErrorMatches, "failed to parse email")
}

func TestVerifyClientCredentials(t *testing.T) {
c := qt.New(t)
ctx := context.Background()

const (
validClientID = "test-client-id"
validClientSecret = "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf"
)

authSvc := setupTestAuthSvc(ctx, c, time.Hour)

Check failure on line 239 in internal/auth/oauth2_test.go

View workflow job for this annotation

GitHub Actions / Build and Test

assignment mismatch: 1 variable but setupTestAuthSvc returns 2 values

err := authSvc.VerifyClientCredentials(ctx, validClientID, validClientSecret)
c.Assert(err, qt.IsNil)

err = authSvc.VerifyClientCredentials(ctx, "invalid-client-id", validClientSecret)
c.Assert(err, qt.ErrorMatches, "invalid client credentials")
}
3 changes: 3 additions & 0 deletions internal/jimm/jimm.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ type OAuthAuthenticator interface {
// UpdateIdentity updates the database with the display name and access token set for the user.
// And, if present, a refresh token.
UpdateIdentity(ctx context.Context, email string, token *oauth2.Token) error

// VerifyClientCredentials verifies the provided client ID and client secret.
VerifyClientCredentials(ctx context.Context, clientID string, clientSecret string) error
}

type permission struct {
Expand Down
46 changes: 46 additions & 0 deletions internal/jujuapi/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,52 @@ func (r *controllerRoot) LoginWithSessionToken(ctx context.Context, req params.L
}, nil
}

// LoginWithClientCredentials handles logging into the JIMM with the client ID
// and secret created by the IdP.
func (r *controllerRoot) LoginWithClientCredentials(ctx context.Context, req params.LoginWithClientCredentialsRequest) (jujuparams.LoginResult, error) {
const op = errors.Op("jujuapi.LoginWithSessionToken")

authenticationSvc := r.jimm.OAuthAuthenticationService()
if authenticationSvc == nil {
return jujuparams.LoginResult{}, errors.E("authentication service not specified")
}
err := authenticationSvc.VerifyClientCredentials(ctx, req.ClientID, req.ClientSecret)
if err != nil {
return jujuparams.LoginResult{}, errors.E(err, errors.CodeUnauthorized)
}

// Get an OpenFGA user to place on the controllerRoot for this WS
// such that:
//
// - Subsequent calls are aware of the user
// - Authorisation checks are done against the openfga.User

// At this point, we know the user exists, so simply just get
// the user to create the session token.
user, err := r.jimm.GetOpenFGAUserAndAuthorise(ctx, req.ClientID)
if err != nil {
return jujuparams.LoginResult{}, errors.E(op, err)
}

r.mu.Lock()
r.user = user
r.mu.Unlock()

// Get server version for LoginResult
srvVersion, err := r.jimm.EarliestControllerVersion(ctx)
if err != nil {
return jujuparams.LoginResult{}, errors.E(op, err)
}

return jujuparams.LoginResult{
PublicDNSName: r.params.PublicDNSName,
UserInfo: setupAuthUserInfo(ctx, r, user),
ControllerTag: setupControllerTag(r),
Facades: setupFacades(r),
ServerVersion: srvVersion.String(),
}, nil
}

// setupControllerTag returns the String() of a controller tag based on the
// JIMM controller UUID.
func setupControllerTag(root *controllerRoot) string {
Expand Down
28 changes: 28 additions & 0 deletions internal/jujuapi/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/coreos/go-oidc/v3/oidc"
"github.com/juju/juju/api"
jujuparams "github.com/juju/juju/rpc/params"
"github.com/juju/names/v4"
gc "gopkg.in/check.v1"
"gopkg.in/macaroon.v2"
)
Expand Down Expand Up @@ -216,3 +217,30 @@ func handleLoginForm(c *gc.C, loginForm string, client *http.Client, username, p
re = regexp.MustCompile(`Device Login Successful`)
c.Assert(re.MatchString(string(b)), gc.Equals, true)
}

func (s *adminSuite) TestLoginWithClientCredentials(c *gc.C) {
conn := s.open(c, &api.Info{
SkipLogin: true,
}, "test")
defer conn.Close()

const (
validClientID = "test-client-id"
validClientSecret = "2M2blFbO4GX4zfggQpivQSxwWX1XGgNf"
)

var loginResult jujuparams.LoginResult
err := conn.APICall("Admin", 4, "", "LoginWithClientCredentials", params.LoginWithClientCredentialsRequest{
ClientID: validClientID,
ClientSecret: validClientSecret,
}, &loginResult)
c.Assert(err, gc.IsNil)
c.Assert(loginResult.ControllerTag, gc.Equals, names.NewControllerTag(s.Params.ControllerUUID).String())
c.Assert(loginResult.UserInfo.Identity, gc.Equals, names.NewUserTag("test-client-id").String())

err = conn.APICall("Admin", 4, "", "LoginWithClientCredentials", params.LoginWithClientCredentialsRequest{
ClientID: "invalid-client-id",
ClientSecret: "invalid-secret",
}, &loginResult)
c.Assert(err, gc.ErrorMatches, `invalid client credentials \(unauthorized access\)`)
}
1 change: 1 addition & 0 deletions internal/jujuapi/controllerroot.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ func newControllerRoot(j JIMM, p Params) *controllerRoot {
r.AddMethod("Admin", 4, "LoginDevice", rpc.Method(r.LoginDevice))
r.AddMethod("Admin", 4, "GetDeviceSessionToken", rpc.Method(r.GetDeviceSessionToken))
r.AddMethod("Admin", 4, "LoginWithSessionToken", rpc.Method(r.LoginWithSessionToken))
r.AddMethod("Admin", 4, "LoginWithClientCredentials", rpc.Method(r.LoginWithClientCredentials))
r.AddMethod("Pinger", 1, "Ping", rpc.Method(r.Ping))
return r
}
Expand Down

0 comments on commit 2366cf9

Please sign in to comment.