198 lines
4.5 KiB
Go
198 lines
4.5 KiB
Go
|
// Copyright (c) Mainflux
|
||
|
// SPDX-License-Identifier: Apache-2.0
|
||
|
|
||
|
package authn
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"errors"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
loginDuration = 10 * time.Hour
|
||
|
resetDuration = 5 * time.Minute
|
||
|
issuerName = "mainflux.authn"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
// ErrUnauthorizedAccess represents unauthorized access.
|
||
|
ErrUnauthorizedAccess = errors.New("unauthorized access")
|
||
|
|
||
|
// ErrMalformedEntity indicates malformed entity specification (e.g.
|
||
|
// invalid owner or ID).
|
||
|
ErrMalformedEntity = errors.New("malformed entity specification")
|
||
|
|
||
|
// ErrNotFound indicates a non-existing entity request.
|
||
|
ErrNotFound = errors.New("entity not found")
|
||
|
|
||
|
// ErrConflict indicates that entity already exists.
|
||
|
ErrConflict = errors.New("entity already exists")
|
||
|
)
|
||
|
|
||
|
// Service specifies an API that must be fullfiled by the domain service
|
||
|
// implementation, and all of its decorators (e.g. logging & metrics).
|
||
|
type Service interface {
|
||
|
// Issue issues a new Key.
|
||
|
Issue(context.Context, string, Key) (Key, error)
|
||
|
|
||
|
// Revoke removes the Key with the provided id that is
|
||
|
// issued by the user identified by the provided key.
|
||
|
Revoke(context.Context, string, string) error
|
||
|
|
||
|
// Retrieve retrieves data for the Key identified by the provided
|
||
|
// ID, that is issued by the user identified by the provided key.
|
||
|
Retrieve(context.Context, string, string) (Key, error)
|
||
|
|
||
|
// Identify validates token token. If token is valid, content
|
||
|
// is returned. If token is invalid, or invocation failed for some
|
||
|
// other reason, non-nil error value is returned in response.
|
||
|
Identify(context.Context, string) (string, error)
|
||
|
}
|
||
|
|
||
|
var _ Service = (*service)(nil)
|
||
|
|
||
|
type service struct {
|
||
|
keys KeyRepository
|
||
|
idp IdentityProvider
|
||
|
tokenizer Tokenizer
|
||
|
}
|
||
|
|
||
|
// New instantiates the auth service implementation.
|
||
|
func New(keys KeyRepository, idp IdentityProvider, tokenizer Tokenizer) Service {
|
||
|
return &service{
|
||
|
tokenizer: tokenizer,
|
||
|
keys: keys,
|
||
|
idp: idp,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (svc service) Issue(ctx context.Context, issuer string, key Key) (Key, error) {
|
||
|
if key.IssuedAt.IsZero() {
|
||
|
return Key{}, ErrInvalidKeyIssuedAt
|
||
|
}
|
||
|
switch key.Type {
|
||
|
case APIKey:
|
||
|
return svc.userKey(ctx, issuer, key)
|
||
|
case RecoveryKey:
|
||
|
return svc.resetKey(ctx, issuer, key)
|
||
|
default:
|
||
|
return svc.loginKey(issuer, key)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (svc service) Revoke(ctx context.Context, issuer, id string) error {
|
||
|
email, err := svc.login(issuer)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return svc.keys.Remove(ctx, email, id)
|
||
|
}
|
||
|
|
||
|
func (svc service) Retrieve(ctx context.Context, issuer, id string) (Key, error) {
|
||
|
email, err := svc.login(issuer)
|
||
|
if err != nil {
|
||
|
return Key{}, err
|
||
|
}
|
||
|
|
||
|
return svc.keys.Retrieve(ctx, email, id)
|
||
|
}
|
||
|
|
||
|
func (svc service) Identify(ctx context.Context, token string) (string, error) {
|
||
|
c, err := svc.tokenizer.Parse(token)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
switch c.Type {
|
||
|
case APIKey:
|
||
|
k, err := svc.keys.Retrieve(ctx, c.Issuer, c.ID)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
// Auto revoke expired key.
|
||
|
if k.Expired() {
|
||
|
svc.keys.Remove(ctx, c.Issuer, c.ID)
|
||
|
return "", ErrKeyExpired
|
||
|
}
|
||
|
return c.Issuer, nil
|
||
|
case RecoveryKey, UserKey:
|
||
|
if c.Issuer != issuerName {
|
||
|
return "", ErrUnauthorizedAccess
|
||
|
}
|
||
|
return c.Secret, nil
|
||
|
default:
|
||
|
return "", ErrUnauthorizedAccess
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (svc service) loginKey(issuer string, key Key) (Key, error) {
|
||
|
key.Secret = issuer
|
||
|
return svc.tempKey(loginDuration, key)
|
||
|
}
|
||
|
|
||
|
func (svc service) resetKey(ctx context.Context, issuer string, key Key) (Key, error) {
|
||
|
issuer, err := svc.login(issuer)
|
||
|
if err != nil {
|
||
|
return Key{}, err
|
||
|
}
|
||
|
key.Secret = issuer
|
||
|
|
||
|
return svc.tempKey(resetDuration, key)
|
||
|
}
|
||
|
|
||
|
func (svc service) tempKey(duration time.Duration, key Key) (Key, error) {
|
||
|
key.Issuer = issuerName
|
||
|
key.ExpiresAt = key.IssuedAt.Add(duration)
|
||
|
val, err := svc.tokenizer.Issue(key)
|
||
|
if err != nil {
|
||
|
return Key{}, err
|
||
|
}
|
||
|
|
||
|
key.Secret = val
|
||
|
return key, nil
|
||
|
}
|
||
|
|
||
|
func (svc service) userKey(ctx context.Context, issuer string, key Key) (Key, error) {
|
||
|
email, err := svc.login(issuer)
|
||
|
if err != nil {
|
||
|
return Key{}, err
|
||
|
}
|
||
|
key.Issuer = email
|
||
|
|
||
|
id, err := svc.idp.ID()
|
||
|
if err != nil {
|
||
|
return Key{}, err
|
||
|
}
|
||
|
key.ID = id
|
||
|
|
||
|
value, err := svc.tokenizer.Issue(key)
|
||
|
if err != nil {
|
||
|
return Key{}, err
|
||
|
}
|
||
|
key.Secret = value
|
||
|
|
||
|
if _, err := svc.keys.Save(ctx, key); err != nil {
|
||
|
return Key{}, err
|
||
|
}
|
||
|
|
||
|
return key, nil
|
||
|
}
|
||
|
|
||
|
func (svc service) login(token string) (string, error) {
|
||
|
c, err := svc.tokenizer.Parse(token)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
// Only user key token is valid for login.
|
||
|
if c.Type != UserKey {
|
||
|
return "", ErrUnauthorizedAccess
|
||
|
}
|
||
|
|
||
|
if c.Secret == "" {
|
||
|
return "", ErrUnauthorizedAccess
|
||
|
}
|
||
|
return c.Secret, nil
|
||
|
}
|