AuthN: Login (#61225)

* AuthN: Add function to login auth request
This commit is contained in:
Karl Persson 2023-01-10 14:55:27 +01:00 committed by GitHub
parent ef9a71f483
commit 2de72c1c39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 132 additions and 11 deletions

View File

@ -34,10 +34,12 @@ type ClientParams struct {
type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error
type Service interface {
// RegisterPostAuthHook registers a hook that is called after a successful authentication.
RegisterPostAuthHook(hook PostAuthHookFn)
// 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)
}
type Client interface {

View File

@ -5,7 +5,10 @@ import (
"net/http"
"strconv"
"go.opentelemetry.io/otel/attribute"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/network"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey"
@ -20,7 +23,7 @@ import (
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"go.opentelemetry.io/otel/attribute"
"github.com/grafana/grafana/pkg/web"
)
// make sure service implements authn.Service interface
@ -35,11 +38,12 @@ func ProvideService(
authInfoService login.AuthInfoService, renderService rendering.Service,
) *Service {
s := &Service{
log: log.New("authn.service"),
cfg: cfg,
clients: make(map[string]authn.Client),
tracer: tracer,
postAuthHooks: []authn.PostAuthHookFn{},
log: log.New("authn.service"),
cfg: cfg,
clients: make(map[string]authn.Client),
tracer: tracer,
sessionService: sessionService,
postAuthHooks: []authn.PostAuthHookFn{},
}
s.clients[authn.ClientRender] = clients.ProvideRender(userService, renderService)
@ -81,9 +85,12 @@ type Service struct {
log log.Logger
cfg *setting.Cfg
clients map[string]authn.Client
tracer tracing.Tracer
sessionService auth.UserTokenService
// postAuthHooks are called after a successful authentication. They can modify the identity.
postAuthHooks []authn.PostAuthHookFn
tracer tracing.Tracer
}
func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Request) (*authn.Identity, bool, error) {
@ -117,6 +124,40 @@ 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)
if !ok {
return nil, authn.ErrClientNotConfigured.Errorf("client not configured: %s", client)
}
if err != nil {
return nil, err
}
namespace, id := identity.NamespacedID()
// Login is only supported for users
if namespace != authn.NamespaceUser {
return nil, authn.ErrUnsupportedIdentity.Errorf("expected identity of type user but got: %s", namespace)
}
addr := web.RemoteAddr(r.HTTPRequest)
ip, err := network.GetIPFromAddress(addr)
if err != nil {
s.log.Debug("failed to parse ip from address", "addr", addr)
}
sessionToken, err := s.sessionService.CreateToken(ctx, &user.User{ID: id}, ip, r.HTTPRequest.UserAgent())
if err != nil {
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)
}

View File

@ -3,10 +3,14 @@ package authnimpl
import (
"context"
"errors"
"net"
"net/http"
"net/url"
"testing"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/infra/log"
@ -124,6 +128,76 @@ func TestService_AuthenticateOrgID(t *testing.T) {
}
}
func TestService_Login(t *testing.T) {
type TestCase struct {
desc string
client string
expectedClientOK bool
expectedClientErr error
expectedClientIdentity *authn.Identity
expectedSessionErr error
expectedErr error
expectedIdentity *authn.Identity
}
tests := []TestCase{
{
desc: "should authenticate and create session for valid request",
client: "fake",
expectedClientOK: true,
expectedClientIdentity: &authn.Identity{
ID: "user:1",
},
expectedIdentity: &authn.Identity{
ID: "user:1",
SessionToken: &auth.UserToken{UserId: 1},
},
},
{
desc: "should not authenticate with invalid client",
client: "invalid",
expectedErr: authn.ErrClientNotConfigured,
},
{
desc: "should not authenticate non user identity",
client: "fake",
expectedClientOK: true,
expectedClientIdentity: &authn.Identity{ID: "apikey:1"},
expectedErr: authn.ErrUnsupportedIdentity,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
s := setupTests(t, func(svc *Service) {
svc.clients["fake"] = &authntest.FakeClient{
ExpectedErr: tt.expectedClientErr,
ExpectedTest: tt.expectedClientOK,
ExpectedIdentity: tt.expectedClientIdentity,
}
svc.sessionService = &authtest.FakeUserAuthTokenService{
CreateTokenProvider: func(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) {
if tt.expectedSessionErr != nil {
return nil, tt.expectedSessionErr
}
return &auth.UserToken{UserId: user.ID}, nil
},
}
})
identity, err := s.Login(context.Background(), tt.client, &authn.Request{HTTPRequest: &http.Request{
Header: map[string][]string{},
URL: &url.URL{},
}})
assert.ErrorIs(t, err, tt.expectedErr)
assert.EqualValues(t, tt.expectedIdentity, identity)
})
}
}
func mustParseURL(s string) *url.URL {
u, err := url.Parse(s)
if err != nil {

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/services/loginattempt"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/grafana/grafana/pkg/web"
)
var (
@ -54,7 +55,7 @@ func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
}
if errors.Is(err, errInvalidPassword) {
// only add login attempt if identity was found but the provided password was invalid
_ = c.loginAttempts.Add(ctx, username, r.HTTPRequest.RemoteAddr)
_ = c.loginAttempts.Add(ctx, username, web.RemoteAddr(r.HTTPRequest))
}
return nil, errBasicAuthCredentials.Errorf("failed to authenticate identity: %w", err)
}

View File

@ -2,4 +2,7 @@ package authn
import "github.com/grafana/grafana/pkg/util/errutil"
var ErrClientNotFound = errutil.NewBase(errutil.StatusNotFound, "auth.client.notConfigured")
var (
ErrClientNotConfigured = errutil.NewBase(errutil.StatusBadRequest, "auth.client.notConfigured")
ErrUnsupportedIdentity = errutil.NewBase(errutil.StatusNotImplemented, "auth.identity.unsupported")
)