AuthN: Post login hooks (#61287)

* AuthN: add the ability to register post login hooks

* AuthN: add a guard for the user id

* AuthN: Add helper to create external user info from identity

* AuthN: Pass auth request to password clients

* AuthN: set auth module and username in metadata
This commit is contained in:
Karl Persson 2023-01-12 15:02:04 +01:00 committed by GitHub
parent 4b13a5a9ab
commit 3e8857acb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 90 additions and 27 deletions

View File

@ -8,12 +8,13 @@ import (
"strings"
"time"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/web"
"golang.org/x/oauth2"
)
const (
@ -25,7 +26,12 @@ const (
ClientSession = "auth.client.session"
)
// ClientParams are hints to the auth service about how to handle the identity management
const (
MetaKeyUsername = "username"
MetaKeyAuthModule = "authModule"
)
// ClientParams are hints to the auth serviAuthN: Post login hooksce about how to handle the identity management
// from the authenticating client.
type ClientParams struct {
// Update the internal representation of the entity from the identity provided
@ -41,14 +47,17 @@ type ClientParams struct {
}
type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error
type PostLoginHookFn func(ctx context.Context, identity *Identity, r *Request, err error)
type Service interface {
// Authenticate authenticates a request using the specified client.
Authenticate(ctx context.Context, client string, r *Request) (*Identity, bool, error)
// Login authenticates a request and creates a session on successful authentication.
Login(ctx context.Context, client string, r *Request) (*Identity, error)
// RegisterPostAuthHook registers a hook that is called after a successful authentication.
RegisterPostAuthHook(hook PostAuthHookFn)
// Login authenticates a request and creates a session on successful authentication.
Login(ctx context.Context, client string, r *Request) (*Identity, error)
// RegisterPostLoginHook registers a hook that that is called after a login request.
RegisterPostLoginHook(hook PostLoginHookFn)
}
type Client interface {
@ -59,7 +68,7 @@ type Client interface {
}
type PasswordClient interface {
AuthenticatePassword(ctx context.Context, orgID int64, username, password string) (*Identity, error)
AuthenticatePassword(ctx context.Context, r *Request, username, password string) (*Identity, error)
}
type Request struct {
@ -71,6 +80,23 @@ type Request struct {
// Resp is the response writer to use for the request
// Used to set cookies and headers
Resp web.ResponseWriter
// metadata is additional information about the auth request
metadata map[string]string
}
func (r *Request) SetMeta(k, v string) {
if r.metadata == nil {
r.metadata = map[string]string{}
}
r.metadata[k] = v
}
func (r *Request) GetMeta(k string) string {
if r.metadata == nil {
r.metadata = map[string]string{}
}
return r.metadata[k]
}
const (
@ -201,6 +227,23 @@ func (i *Identity) SignedInUser() *user.SignedInUser {
return u
}
func (i *Identity) ExternalUserInfo() models.ExternalUserInfo {
_, id := i.NamespacedID()
return models.ExternalUserInfo{
OAuthToken: i.OAuthToken,
AuthModule: i.AuthModule,
AuthId: i.AuthID,
UserId: id,
Email: i.Email,
Login: i.Login,
Name: i.Name,
Groups: i.Groups,
OrgRoles: i.OrgRoles,
IsGrafanaAdmin: i.IsGrafanaAdmin,
IsDisabled: i.IsDisabled,
}
}
// IdentityFromSignedInUser creates an identity from a SignedInUser.
func IdentityFromSignedInUser(id string, usr *user.SignedInUser, params ClientParams) *Identity {
return &Identity{

View File

@ -96,6 +96,8 @@ type Service struct {
// postAuthHooks are called after a successful authentication. They can modify the identity.
postAuthHooks []authn.PostAuthHookFn
// postLoginHooks are called after a login request is performed, both for failing and successful requests.
postLoginHooks []authn.PostLoginHookFn
}
func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Request) (*authn.Identity, bool, error) {
@ -130,12 +132,23 @@ func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Requ
return identity, true, nil
}
func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (*authn.Identity, error) {
identity, ok, err := s.Authenticate(ctx, client, r)
func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn) {
s.postAuthHooks = append(s.postAuthHooks, hook)
}
func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (identity *authn.Identity, err error) {
var ok bool
identity, ok, err = s.Authenticate(ctx, client, r)
if !ok {
return nil, authn.ErrClientNotConfigured.Errorf("client not configured: %s", client)
}
defer func() {
for _, hook := range s.postLoginHooks {
hook(ctx, identity, r, err)
}
}()
if err != nil {
return nil, err
}
@ -143,7 +156,7 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (*
namespace, id := identity.NamespacedID()
// Login is only supported for users
if namespace != authn.NamespaceUser {
if namespace != authn.NamespaceUser || id <= 0 {
return nil, authn.ErrUnsupportedIdentity.Errorf("expected identity of type user but got: %s", namespace)
}
@ -158,14 +171,12 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (*
return nil, err
}
// FIXME: add login hooks to replace the one used in HookService
identity.SessionToken = sessionToken
return identity, nil
}
func (s *Service) RegisterPostAuthHook(hook authn.PostAuthHookFn) {
s.postAuthHooks = append(s.postAuthHooks, hook)
func (s *Service) RegisterPostLoginHook(hook authn.PostLoginHookFn) {
s.postLoginHooks = append(s.postLoginHooks, hook)
}
func orgIDFromRequest(r *authn.Request) int64 {

View File

@ -33,6 +33,6 @@ type FakePasswordClient struct {
ExpectedIdentity *authn.Identity
}
func (f FakePasswordClient) AuthenticatePassword(ctx context.Context, orgID int64, username, password string) (*authn.Identity, error) {
func (f FakePasswordClient) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) {
return f.ExpectedIdentity, f.ExpectedErr
}

View File

@ -34,6 +34,8 @@ func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
return nil, errDecodingBasicAuthHeader.Errorf("failed to decode basic auth header: %w", err)
}
r.SetMeta(authn.MetaKeyUsername, username)
ok, err := c.loginAttempts.Validate(ctx, username)
if err != nil {
return nil, err
@ -47,7 +49,7 @@ func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
}
for _, pwClient := range c.clients {
identity, err := pwClient.AuthenticatePassword(ctx, r.OrgID, username, password)
identity, err := pwClient.AuthenticatePassword(ctx, r, username, password)
if err != nil {
if errors.Is(err, errIdentityNotFound) {
// continue to next password client if identity could not be found

View File

@ -20,7 +20,7 @@ type Grafana struct {
userService user.Service
}
func (c Grafana) AuthenticatePassword(ctx context.Context, orgID int64, username, password string) (*authn.Identity, error) {
func (c Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) {
usr, err := c.userService.GetByLogin(ctx, &user.GetUserByLoginQuery{LoginOrEmail: username})
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {
@ -29,11 +29,14 @@ func (c Grafana) AuthenticatePassword(ctx context.Context, orgID int64, username
return nil, err
}
// user was found so set auth module in req metadata
r.SetMeta(authn.MetaKeyAuthModule, "grafana")
if ok := comparePassword(password, usr.Salt, usr.Password); !ok {
return nil, errInvalidPassword.Errorf("invalid password")
}
signedInUser, err := c.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{OrgID: orgID, UserID: usr.ID})
signedInUser, err := c.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{OrgID: r.OrgID, UserID: usr.ID})
if err != nil {
return nil, err
}

View File

@ -61,7 +61,7 @@ func TestGrafana_AuthenticatePassword(t *testing.T) {
}
c := ProvideGrafana(userService)
identity, err := c.AuthenticatePassword(context.Background(), 1, tt.username, tt.password)
identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password)
assert.ErrorIs(t, err, tt.expectedErr)
assert.EqualValues(t, tt.expectedIdentity, identity)
})

View File

@ -21,26 +21,30 @@ type LDAP struct {
service ldapService
}
func (c *LDAP) AuthenticatePassword(ctx context.Context, orgID int64, username, password string) (*authn.Identity, error) {
func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) {
info, err := c.service.Login(&models.LoginUserQuery{
Username: username,
Password: password,
})
if err != nil {
if errors.Is(err, multildap.ErrInvalidCredentials) {
return nil, errInvalidPassword.Errorf("invalid password: %w", err)
}
if errors.Is(err, multildap.ErrCouldNotFindUser) {
return nil, errIdentityNotFound.Errorf("no user found: %w", err)
}
// user was found so set auth module in req metadata
r.SetMeta(authn.MetaKeyAuthModule, "ldap")
if errors.Is(err, multildap.ErrInvalidCredentials) {
// FIXME: disable user in grafana if not found
if errors.Is(err, multildap.ErrCouldNotFindUser) {
return nil, errIdentityNotFound.Errorf("no user found: %w", err)
}
return nil, errInvalidPassword.Errorf("invalid password: %w", err)
}
if err != nil {
return nil, err
}
return &authn.Identity{
OrgID: orgID,
OrgID: r.OrgID,
OrgRoles: info.OrgRoles,
Login: info.Login,
Name: info.Name,

View File

@ -79,7 +79,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) {
t.Run(tt.desc, func(t *testing.T) {
c := &LDAP{cfg: setting.NewCfg(), service: fakeLDAPService{ExpectedInfo: tt.expectedInfo, ExpectedErr: tt.expectedLDAPErr}}
identity, err := c.AuthenticatePassword(context.Background(), 1, tt.username, tt.password)
identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password)
assert.ErrorIs(t, err, tt.expectedErr)
assert.EqualValues(t, tt.expectedIdentity, identity)
})