diff --git a/Makefile b/Makefile index 0ca15f14fb..823e165ffd 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 MF_DOCKER_IMAGE_NAME_PREFIX ?= mainflux +MF_RELEASE ?= latest BUILD_DIR = build SERVICES = users things http coap lora influxdb-writer influxdb-reader mongodb-writer \ mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader cli \ @@ -23,7 +24,7 @@ define make_docker --build-arg SVC=$(svc) \ --build-arg GOARCH=$(GOARCH) \ --build-arg GOARM=$(GOARM) \ - --tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \ + --tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc):$(MF_RELEASE) \ -f docker/Dockerfile . endef @@ -33,7 +34,7 @@ define make_docker_dev docker build \ --no-cache \ --build-arg SVC=$(svc) \ - --tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \ + --tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc):$(MF_RELEASE) \ -f docker/Dockerfile.dev ./build endef diff --git a/cmd/things/main.go b/cmd/things/main.go index 4b130f8e19..33e5af1cde 100644 --- a/cmd/things/main.go +++ b/cmd/things/main.go @@ -68,6 +68,7 @@ const ( defJaegerURL = "" defAuthURL = "localhost:8181" defAuthTimeout = "1s" + defOIDC = "false" envLogLevel = "MF_THINGS_LOG_LEVEL" envDBHost = "MF_THINGS_DB_HOST" @@ -97,6 +98,7 @@ const ( envJaegerURL = "MF_JAEGER_URL" envAuthURL = "MF_AUTH_GRPC_URL" envAuthTimeout = "MF_AUTH_GRPC_TIMEOUT" + envOIDC = "MF_OIDC" ) type config struct { @@ -120,6 +122,7 @@ type config struct { jaegerURL string authURL string authTimeout time.Duration + OIDC bool } func main() { @@ -140,12 +143,16 @@ func main() { db := connectToDB(cfg.dbConfig, logger) defer db.Close() - authTracer, authCloser := initJaeger("auth", cfg.jaegerURL, logger) - defer authCloser.Close() + var auth mainflux.AuthServiceClient + var close func() error + if !cfg.OIDC { + authTracer, authCloser := initJaeger("auth", cfg.jaegerURL, logger) + defer authCloser.Close() - auth, close := createAuthClient(cfg, authTracer, logger) - if close != nil { - defer close() + auth, close = createAuthClient(cfg, authTracer, logger) + if close != nil { + defer close() + } } dbTracer, dbCloser := initJaeger("things_db", cfg.jaegerURL, logger) @@ -154,7 +161,7 @@ func main() { cacheTracer, cacheCloser := initJaeger("things_cache", cfg.jaegerURL, logger) defer cacheCloser.Close() - svc := newService(auth, dbTracer, cacheTracer, db, cacheClient, esClient, logger) + svc := newService(auth, dbTracer, cacheTracer, db, cacheClient, esClient, cfg.OIDC, logger) errs := make(chan error, 2) go startHTTPServer(thhttpapi.MakeHandler(thingsTracer, svc), cfg.httpPort, cfg, logger, errs) @@ -182,6 +189,11 @@ func loadConfig() config { log.Fatalf("Invalid %s value: %s", envAuthTimeout, err.Error()) } + oidc, err := strconv.ParseBool(mainflux.Env(envOIDC, defOIDC)) + if err != nil { + log.Fatalf("Invalid value passed for %s\n", envClientTLS) + } + dbConfig := postgres.Config{ Host: mainflux.Env(envDBHost, defDBHost), Port: mainflux.Env(envDBPort, defDBPort), @@ -215,6 +227,7 @@ func loadConfig() config { jaegerURL: mainflux.Env(envJaegerURL, defJaegerURL), authURL: mainflux.Env(envAuthURL, defAuthURL), authTimeout: authTimeout, + OIDC: oidc, } } @@ -295,11 +308,11 @@ func connectToAuth(cfg config, logger logger.Logger) *grpc.ClientConn { logger.Error(fmt.Sprintf("Failed to connect to auth service: %s", err)) os.Exit(1) } - + logger.Info(fmt.Sprint("Connected to auth service %s", cfg.authURL)) return conn } -func newService(auth mainflux.AuthServiceClient, dbTracer opentracing.Tracer, cacheTracer opentracing.Tracer, db *sqlx.DB, cacheClient *redis.Client, esClient *redis.Client, logger logger.Logger) things.Service { +func newService(auth mainflux.AuthServiceClient, dbTracer opentracing.Tracer, cacheTracer opentracing.Tracer, db *sqlx.DB, cacheClient *redis.Client, esClient *redis.Client, oidc bool, logger logger.Logger) things.Service { database := postgres.NewDatabase(db) thingsRepo := postgres.NewThingRepository(database) @@ -315,7 +328,11 @@ func newService(auth mainflux.AuthServiceClient, dbTracer opentracing.Tracer, ca thingCache = tracing.ThingCacheMiddleware(cacheTracer, thingCache) idProvider := uuid.New() - svc := things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, idProvider) + if oidc { + logger.Info(("Using OIDC authentication")) + } + + svc := things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, oidc, idProvider) svc = rediscache.NewEventStoreMiddleware(svc, esClient) svc = api.LoggingMiddleware(svc, logger) svc = api.MetricsMiddleware( diff --git a/cmd/twins/main.go b/cmd/twins/main.go index 1eeb1eec68..464d1e5371 100644 --- a/cmd/twins/main.go +++ b/cmd/twins/main.go @@ -60,6 +60,7 @@ const ( defNatsURL = "nats://localhost:4222" defAuthURL = "localhost:8181" defAuthTimeout = "1s" + defOIDC = "false" envLogLevel = "MF_TWINS_LOG_LEVEL" envHTTPPort = "MF_TWINS_HTTP_PORT" @@ -80,6 +81,7 @@ const ( envNatsURL = "MF_NATS_URL" envAuthURL = "MF_AUTH_GRPC_URL" envAuthTimeout = "MF_AUTH_GRPC_TIMEOUT" + envOIDC = "MF_OIDC" ) type config struct { @@ -101,6 +103,7 @@ type config struct { authURL string authTimeout time.Duration + OIDC bool } func main() { @@ -123,9 +126,17 @@ func main() { dbTracer, dbCloser := initJaeger("twins_db", cfg.jaegerURL, logger) defer dbCloser.Close() - authTracer, authCloser := initJaeger("auth", cfg.jaegerURL, logger) - defer authCloser.Close() - auth, _ := createAuthClient(cfg, authTracer, logger) + var auth mainflux.AuthServiceClient + var close func() error + if !cfg.OIDC { + authTracer, authCloser := initJaeger("auth", cfg.jaegerURL, logger) + defer authCloser.Close() + + auth, close = createAuthClient(cfg, authTracer, logger) + if close != nil { + defer close() + } + } pubSub, err := nats.NewPubSub(cfg.natsURL, queue, logger) if err != nil { @@ -134,7 +145,7 @@ func main() { } defer pubSub.Close() - svc := newService(pubSub, cfg.channelID, auth, dbTracer, db, cacheTracer, cacheClient, logger) + svc := newService(pubSub, cfg.channelID, auth, dbTracer, db, cacheTracer, cacheClient, cfg.OIDC, logger) tracer, closer := initJaeger("twins", cfg.jaegerURL, logger) defer closer.Close() @@ -168,6 +179,11 @@ func loadConfig() config { Port: mainflux.Env(envDBPort, defDBPort), } + oidc, err := strconv.ParseBool(mainflux.Env(envOIDC, defOIDC)) + if err != nil { + log.Fatalf("Invalid value passed for %s\n", envOIDC) + } + return config{ logLevel: mainflux.Env(envLogLevel, defLogLevel), httpPort: mainflux.Env(envHTTPPort, defHTTPPort), @@ -186,6 +202,7 @@ func loadConfig() config { natsURL: mainflux.Env(envNatsURL, defNatsURL), authURL: mainflux.Env(envAuthURL, defAuthURL), authTimeout: authTimeout, + OIDC: oidc, } } @@ -244,6 +261,8 @@ func connectToAuth(cfg config, logger logger.Logger) *grpc.ClientConn { os.Exit(1) } + logger.Info(fmt.Sprint("Connected to auth service %s", cfg.authURL)) + return conn } @@ -261,18 +280,21 @@ func connectToRedis(cacheURL, cachePass, cacheDB string, logger logger.Logger) * }) } -func newService(ps messaging.PubSub, chanID string, users mainflux.AuthServiceClient, dbTracer opentracing.Tracer, db *mongo.Database, cacheTracer opentracing.Tracer, cacheClient *redis.Client, logger logger.Logger) twins.Service { +func newService(ps messaging.PubSub, chanID string, users mainflux.AuthServiceClient, dbTracer opentracing.Tracer, db *mongo.Database, cacheTracer opentracing.Tracer, cacheClient *redis.Client, oidc bool, logger logger.Logger) twins.Service { twinRepo := twmongodb.NewTwinRepository(db) twinRepo = tracing.TwinRepositoryMiddleware(dbTracer, twinRepo) stateRepo := twmongodb.NewStateRepository(db) stateRepo = tracing.StateRepositoryMiddleware(dbTracer, stateRepo) + if oidc { + logger.Info(("Using OIDC authentication")) + } idProvider := uuid.New() twinCache := rediscache.NewTwinCache(cacheClient) twinCache = tracing.TwinCacheMiddleware(cacheTracer, twinCache) - svc := twins.New(ps, users, twinRepo, twinCache, stateRepo, idProvider, chanID, logger) + svc := twins.New(ps, users, twinRepo, twinCache, stateRepo, idProvider, chanID, oidc, logger) svc = api.LoggingMiddleware(svc, logger) svc = api.MetricsMiddleware( svc, diff --git a/cmd/users/main.go b/cmd/users/main.go index 5f7a6f1314..15974070c8 100644 --- a/cmd/users/main.go +++ b/cmd/users/main.go @@ -74,6 +74,8 @@ const ( defAuthURL = "localhost:8181" defAuthTimeout = "1s" + defOIDC = "false" + envLogLevel = "MF_USERS_LOG_LEVEL" envDBHost = "MF_USERS_DB_HOST" envDBPort = "MF_USERS_DB_PORT" @@ -109,6 +111,8 @@ const ( envAuthCACerts = "MF_AUTH_CA_CERTS" envAuthURL = "MF_AUTH_GRPC_URL" envAuthTimeout = "MF_AUTH_GRPC_TIMEOUT" + + envOIDC = "MF_OIDC" ) type config struct { @@ -127,6 +131,7 @@ type config struct { adminEmail string adminPassword string passRegex *regexp.Regexp + OIDC bool } func main() { @@ -179,6 +184,11 @@ func loadConfig() config { log.Fatalf("Invalid value passed for %s\n", envAuthTLS) } + oidc, err := strconv.ParseBool(mainflux.Env(envOIDC, defOIDC)) + if err != nil { + log.Fatalf("Invalid value passed for %s\n", envAuthTLS) + } + passRegex, err := regexp.Compile(mainflux.Env(envPassRegex, defPassRegex)) if err != nil { log.Fatalf("Invalid password validation rules %s\n", envPassRegex) @@ -222,6 +232,7 @@ func loadConfig() config { authTimeout: authTimeout, adminEmail: mainflux.Env(envAdminEmail, defAdminEmail), adminPassword: mainflux.Env(envAdminPassword, defAdminPassword), + OIDC: oidc, passRegex: passRegex, } @@ -293,10 +304,13 @@ func newService(db *sqlx.DB, tracer opentracing.Tracer, auth mainflux.AuthServic if err != nil { logger.Error(fmt.Sprintf("Failed to configure e-mailing util: %s", err.Error())) } + if c.OIDC { + logger.Info(("Using OIDC authentication")) + } idProvider := uuid.New() - svc := users.New(userRepo, hasher, auth, emailer, idProvider, c.passRegex) + svc := users.New(userRepo, hasher, auth, emailer, idProvider, c.OIDC, c.passRegex) svc = api.LoggingMiddleware(svc, logger) svc = api.MetricsMiddleware( svc, diff --git a/docker/addons/bootstrap/docker-compose.yml b/docker/addons/bootstrap/docker-compose.yml index f117c38db7..c014d7d360 100644 --- a/docker/addons/bootstrap/docker-compose.yml +++ b/docker/addons/bootstrap/docker-compose.yml @@ -52,5 +52,6 @@ services: MF_JAEGER_URL: ${MF_JAEGER_URL} MF_AUTH_GRPC_URL: ${MF_AUTH_GRPC_URL} MF_AUTH_GRPC_TIMMEOUT: ${MF_AUTH_GRPC_TIMEOUT} + MF_OIDC: ${MF_OIDC} networks: - docker_mainflux-base-net diff --git a/docker/addons/cassandra-reader/docker-compose.yml b/docker/addons/cassandra-reader/docker-compose.yml index eeea67b015..5acef1ade3 100644 --- a/docker/addons/cassandra-reader/docker-compose.yml +++ b/docker/addons/cassandra-reader/docker-compose.yml @@ -27,6 +27,7 @@ services: MF_JAEGER_URL: ${MF_JAEGER_URL} MF_THINGS_AUTH_GRPC_URL: ${MF_THINGS_AUTH_GRPC_URL} MF_THINGS_AUTH_GRPC_TIMEOUT: ${MF_THINGS_AUTH_GRPC_TIMEOUT} + MF_OIDC: ${MF_OIDC} ports: - ${MF_CASSANDRA_READER_PORT}:${MF_CASSANDRA_READER_PORT} networks: diff --git a/docker/addons/influxdb-reader/docker-compose.yml b/docker/addons/influxdb-reader/docker-compose.yml index 25ccd6fffb..d047488f94 100644 --- a/docker/addons/influxdb-reader/docker-compose.yml +++ b/docker/addons/influxdb-reader/docker-compose.yml @@ -32,6 +32,7 @@ services: MF_JAEGER_URL: ${MF_JAEGER_URL} MF_THINGS_AUTH_GRPC_URL: ${MF_THINGS_AUTH_GRPC_URL} MF_THINGS_AUTH_GRPC_TIMEOUT: ${MF_THINGS_AUTH_GRPC_TIMEOUT} + MF_OIDC: ${MF_OIDC} ports: - ${MF_INFLUX_READER_PORT}:${MF_INFLUX_READER_PORT} networks: diff --git a/docker/addons/mongodb-reader/docker-compose.yml b/docker/addons/mongodb-reader/docker-compose.yml index ec14081240..45f9d876b6 100644 --- a/docker/addons/mongodb-reader/docker-compose.yml +++ b/docker/addons/mongodb-reader/docker-compose.yml @@ -29,6 +29,7 @@ services: MF_JAEGER_URL: ${MF_JAEGER_URL} MF_THINGS_AUTH_GRPC_URL: ${MF_THINGS_AUTH_GRPC_URL} MF_THINGS_AUTH_GRPC_TIMEOUT: ${MF_THINGS_AUTH_GRPC_TIMEOUT} + MF_OIDC: ${MF_OIDC} ports: - ${MF_MONGO_READER_PORT}:${MF_MONGO_READER_PORT} networks: diff --git a/docker/addons/postgres-reader/docker-compose.yml b/docker/addons/postgres-reader/docker-compose.yml index b4a2f52123..1c86e100c9 100644 --- a/docker/addons/postgres-reader/docker-compose.yml +++ b/docker/addons/postgres-reader/docker-compose.yml @@ -35,6 +35,7 @@ services: MF_JAEGER_URL: ${MF_JAEGER_URL} MF_THINGS_AUTH_GRPC_URL: ${MF_THINGS_AUTH_GRPC_URL} MF_THINGS_AUTH_GRPC_TIMEOUT: ${MF_THINGS_AUTH_GRPC_TIMEOUT} + MF_OIDC: ${MF_OIDC} ports: - ${MF_POSTGRES_READER_PORT}:${MF_POSTGRES_READER_PORT} networks: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a2034a6ccc..97167ab07a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -87,6 +87,7 @@ services: MF_AUTH_GRPC_PORT: ${MF_AUTH_GRPC_PORT} MF_AUTH_SECRET: ${MF_AUTH_SECRET} MF_JAEGER_URL: ${MF_JAEGER_URL} + MF_OIDC: ${MF_OIDC} ports: - ${MF_AUTH_HTTP_PORT}:${MF_AUTH_HTTP_PORT} - ${MF_AUTH_GRPC_PORT}:${MF_AUTH_GRPC_PORT} @@ -136,6 +137,7 @@ services: MF_AUTH_GRPC_TIMEOUT: ${MF_AUTH_GRPC_TIMEOUT} MF_USERS_ADMIN_EMAIL: ${MF_USERS_ADMIN_EMAIL} MF_USERS_ADMIN_PASSWORD: ${MF_USERS_ADMIN_PASSWORD} + MF_OIDC: ${MF_OIDC} ports: - ${MF_USERS_HTTP_PORT}:${MF_USERS_HTTP_PORT} networks: @@ -185,6 +187,7 @@ services: MF_JAEGER_URL: ${MF_JAEGER_URL} MF_AUTH_GRPC_URL: ${MF_AUTH_GRPC_URL} MF_AUTH_GRPC_TIMEOUT: ${MF_AUTH_GRPC_TIMEOUT} + MF_OIDC: ${MF_OIDC} ports: - ${MF_THINGS_HTTP_PORT}:${MF_THINGS_HTTP_PORT} - ${MF_THINGS_AUTH_HTTP_PORT}:${MF_THINGS_AUTH_HTTP_PORT} diff --git a/pkg/sdk/go/users_test.go b/pkg/sdk/go/users_test.go index e7688783d2..9ae4856d2e 100644 --- a/pkg/sdk/go/users_test.go +++ b/pkg/sdk/go/users_test.go @@ -37,7 +37,7 @@ func newUserService() users.Service { emailer := mocks.NewEmailer() idProvider := uuid.New() - return users.New(usersRepo, hasher, auth, emailer, idProvider, passRegex) + return users.New(usersRepo, hasher, auth, emailer, idProvider, false, passRegex) } func newUserServer(svc users.Service) *httptest.Server { diff --git a/readers/api/transport.go b/readers/api/transport.go index 0f85330ab7..9314893338 100644 --- a/readers/api/transport.go +++ b/readers/api/transport.go @@ -6,12 +6,16 @@ package api import ( "context" "encoding/json" + "fmt" "net/http" "strconv" + "strings" "time" kithttp "github.com/go-kit/kit/transport/http" "github.com/go-zoo/bone" + "github.com/gofrs/uuid" + "github.com/golang-jwt/jwt/v4" "github.com/mainflux/mainflux" "github.com/mainflux/mainflux/internal/httputil" "github.com/mainflux/mainflux/pkg/errors" @@ -42,14 +46,18 @@ const ( ) var ( - errUnauthorizedAccess = errors.New("missing or invalid credentials provided") - auth mainflux.ThingsServiceClient + errUnauthorizedAccess = errors.New("missing or invalid credentials provided") + errMissingAuthorizationHeader = errors.New("missing authorization header") + errChannelThingConnectionMissing = errors.New("channel is not connected to a thing") + errNotChannelOwner = errors.New("user is not the owner of channel") + auth mainflux.ThingsServiceClient + oidc bool ) // MakeHandler returns a HTTP handler for API endpoints. func MakeHandler(svc readers.MessageRepository, tc mainflux.ThingsServiceClient, svcName string) http.Handler { auth = tc - + oidc = true opts := []kithttp.ServerOption{ kithttp.ServerErrorEncoder(encodeError), } @@ -74,7 +82,7 @@ func decodeList(_ context.Context, r *http.Request) (interface{}, error) { return nil, errors.ErrInvalidQueryParams } - if err := authorize(r, chanID); err != nil { + if err := authorize(r, chanID, true); err != nil { return nil, err } @@ -210,25 +218,44 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) { } } -func authorize(r *http.Request, chanID string) error { +func authorize(r *http.Request, chanID string, oidc bool) error { token := r.Header.Get("Authorization") if token == "" { return errUnauthorizedAccess } + fmt.Println("authorize reader") ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - _, err := auth.CanAccessByKey(ctx, &mainflux.AccessByKeyReq{Token: token, ChanID: chanID}) - if err != nil { - e, ok := status.FromError(err) - if ok && e.Code() == codes.PermissionDenied { - return errUnauthorizedAccess + if _, err := uuid.FromString(token); err == nil { + thingID, err := auth.CanAccessByKey(ctx, &mainflux.AccessByKeyReq{Token: token, ChanID: chanID}) + if err != nil { + e, ok := status.FromError(err) + if ok && e.Code() == codes.PermissionDenied { + fmt.Println("authorize reader") + return errors.Wrap(errUnauthorizedAccess, errChannelThingConnectionMissing) + } + fmt.Printf("thing id %v no access on %v - %v\n", thingID, chanID, err) + return err + } + fmt.Printf("thing id %v access on %v \n", thingID, chanID) + return nil + } + + if u, err := identify(ctx, token, oidc); err == nil { + _, err := auth.IsChannelOwner(ctx, &mainflux.ChannelOwnerReq{Owner: u.Id, ChanID: chanID}) + if err != nil { + e, ok := status.FromError(err) + if ok && e.Code() == codes.PermissionDenied { + return errors.Wrap(errUnauthorizedAccess, errNotChannelOwner) + } + return err } - return err + return nil } - return nil + return errUnauthorizedAccess } func readBoolValueQuery(r *http.Request, key string) (bool, error) { @@ -248,3 +275,44 @@ func readBoolValueQuery(r *http.Request, key string) (bool, error) { return b, nil } + +func identify(ctx context.Context, token string, oidc bool) (u *mainflux.UserIdentity, err error) { + fmt.Printf("identify using token %v\n", oidc) + token = strings.ReplaceAll(token, "Bearer ", "") + if len(token) == 0 { + return nil, errors.Wrap(errUnauthorizedAccess, errors.New("Empty authorization token")) + } + if oidc { + // if parsed == nil || !parsed.Valid { + // return nil, errors.Wrap(ErrUnauthorizedAccess, errors.New("Invalid authorization token")) + // } + // TODO - Token should be properly validated + // to validate we need a public key + // it should be possible to get a public key from the token itself + parsed, err := jwt.Parse(token, nil) + if err != nil { + fmt.Println(err) + } + if parsed == nil { + return nil, errors.Wrap(errUnauthorizedAccess, errors.New("Invalid authorization token")) + } + + claims, _ := parsed.Claims.(jwt.MapClaims) + id, ok := claims["sub"].(string) + if !ok { + return nil, errors.Wrap(errUnauthorizedAccess, errors.New("Missing claim sub")) + } + email, ok := claims["email"].(string) + if !ok { + return nil, errors.Wrap(errUnauthorizedAccess, errors.New("Missing email in token")) + } + fmt.Printf("User %v, with email %v\n", id, email) + + return &mainflux.UserIdentity{ + Id: id, + Email: email, + }, nil + } + + return nil, errors.New("Reading with user token implemented only in OIDC mode") +} diff --git a/things/service.go b/things/service.go index ab1a6b00f9..671f026afa 100644 --- a/things/service.go +++ b/things/service.go @@ -5,7 +5,10 @@ package things import ( "context" + "fmt" + "strings" + "github.com/golang-jwt/jwt/v4" "github.com/mainflux/mainflux/pkg/errors" "github.com/mainflux/mainflux" @@ -143,25 +146,60 @@ type thingsService struct { channels ChannelRepository channelCache ChannelCache thingCache ThingCache + OIDC bool idProvider mainflux.IDProvider ulidProvider mainflux.IDProvider } // New instantiates the things service implementation. -func New(auth mainflux.AuthServiceClient, things ThingRepository, channels ChannelRepository, ccache ChannelCache, tcache ThingCache, idp mainflux.IDProvider) Service { +func New(auth mainflux.AuthServiceClient, things ThingRepository, channels ChannelRepository, ccache ChannelCache, tcache ThingCache, oidc bool, idp mainflux.IDProvider) Service { return &thingsService{ auth: auth, things: things, channels: channels, channelCache: ccache, thingCache: tcache, + OIDC: oidc, idProvider: idp, ulidProvider: ulid.New(), } } +func (ts *thingsService) identify(ctx context.Context, token string) (u *mainflux.UserIdentity, err error) { + token = strings.ReplaceAll(token, "Bearer ", "") + if len(token) == 0 { + return nil, errors.Wrap(ErrUnauthorizedAccess, errors.New("Empty authorization token")) + } + if ts.OIDC { + parsed, err := jwt.Parse(token, nil) + if err != nil { + fmt.Println(err) + } + if parsed == nil { + return nil, errors.Wrap(ErrUnauthorizedAccess, errors.New("Invalid authorization token")) + } + + claims, _ := parsed.Claims.(jwt.MapClaims) + id, ok := claims["sub"].(string) + if !ok { + return nil, errors.Wrap(ErrUnauthorizedAccess, errors.New("Missing claim sub")) + } + email, ok := claims["email"].(string) + if !ok { + return nil, errors.Wrap(ErrUnauthorizedAccess, errors.New("Missing email in token")) + } + + return &mainflux.UserIdentity{ + Id: id, + Email: email, + }, nil + } + + return ts.auth.Identify(ctx, &mainflux.Token{Value: token}) +} + func (ts *thingsService) CreateThings(ctx context.Context, token string, things ...Thing) ([]Thing, error) { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return []Thing{}, errors.Wrap(ErrUnauthorizedAccess, err) } @@ -172,7 +210,7 @@ func (ts *thingsService) CreateThings(ctx context.Context, token string, things return []Thing{}, errors.Wrap(ErrCreateUUID, err) } - things[i].Owner = res.GetEmail() + things[i].Owner = res.GetId() if things[i].Key == "" { things[i].Key, err = ts.idProvider.ID() @@ -186,56 +224,56 @@ func (ts *thingsService) CreateThings(ctx context.Context, token string, things } func (ts *thingsService) UpdateThing(ctx context.Context, token string, thing Thing) error { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return errors.Wrap(ErrUnauthorizedAccess, err) } - thing.Owner = res.GetEmail() + thing.Owner = res.GetId() return ts.things.Update(ctx, thing) } func (ts *thingsService) UpdateKey(ctx context.Context, token, id, key string) error { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return errors.Wrap(ErrUnauthorizedAccess, err) } - owner := res.GetEmail() + owner := res.GetId() return ts.things.UpdateKey(ctx, owner, id, key) } func (ts *thingsService) ViewThing(ctx context.Context, token, id string) (Thing, error) { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return Thing{}, errors.Wrap(ErrUnauthorizedAccess, err) } - return ts.things.RetrieveByID(ctx, res.GetEmail(), id) + return ts.things.RetrieveByID(ctx, res.GetId(), id) } func (ts *thingsService) ListThings(ctx context.Context, token string, pm PageMetadata) (Page, error) { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return Page{}, errors.Wrap(ErrUnauthorizedAccess, err) } - return ts.things.RetrieveAll(ctx, res.GetEmail(), pm) + return ts.things.RetrieveAll(ctx, res.GetId(), pm) } func (ts *thingsService) ListThingsByChannel(ctx context.Context, token, chID string, pm PageMetadata) (Page, error) { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return Page{}, errors.Wrap(ErrUnauthorizedAccess, err) } - return ts.things.RetrieveByChannel(ctx, res.GetEmail(), chID, pm) + return ts.things.RetrieveByChannel(ctx, res.GetId(), chID, pm) } func (ts *thingsService) RemoveThing(ctx context.Context, token, id string) error { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return errors.Wrap(ErrUnauthorizedAccess, err) } @@ -243,11 +281,11 @@ func (ts *thingsService) RemoveThing(ctx context.Context, token, id string) erro if err := ts.thingCache.Remove(ctx, id); err != nil { return err } - return ts.things.Remove(ctx, res.GetEmail(), id) + return ts.things.Remove(ctx, res.GetId(), id) } func (ts *thingsService) CreateChannels(ctx context.Context, token string, channels ...Channel) ([]Channel, error) { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return []Channel{}, errors.Wrap(ErrUnauthorizedAccess, err) } @@ -258,51 +296,51 @@ func (ts *thingsService) CreateChannels(ctx context.Context, token string, chann return []Channel{}, errors.Wrap(ErrCreateUUID, err) } - channels[i].Owner = res.GetEmail() + channels[i].Owner = res.GetId() } return ts.channels.Save(ctx, channels...) } func (ts *thingsService) UpdateChannel(ctx context.Context, token string, channel Channel) error { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return errors.Wrap(ErrUnauthorizedAccess, err) } - channel.Owner = res.GetEmail() + channel.Owner = res.GetId() return ts.channels.Update(ctx, channel) } func (ts *thingsService) ViewChannel(ctx context.Context, token, id string) (Channel, error) { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return Channel{}, errors.Wrap(ErrUnauthorizedAccess, err) } - return ts.channels.RetrieveByID(ctx, res.GetEmail(), id) + return ts.channels.RetrieveByID(ctx, res.GetId(), id) } func (ts *thingsService) ListChannels(ctx context.Context, token string, pm PageMetadata) (ChannelsPage, error) { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return ChannelsPage{}, errors.Wrap(ErrUnauthorizedAccess, err) } - return ts.channels.RetrieveAll(ctx, res.GetEmail(), pm) + return ts.channels.RetrieveAll(ctx, res.GetId(), pm) } func (ts *thingsService) ListChannelsByThing(ctx context.Context, token, thID string, pm PageMetadata) (ChannelsPage, error) { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return ChannelsPage{}, errors.Wrap(ErrUnauthorizedAccess, err) } - return ts.channels.RetrieveByThing(ctx, res.GetEmail(), thID, pm) + return ts.channels.RetrieveByThing(ctx, res.GetId(), thID, pm) } func (ts *thingsService) RemoveChannel(ctx context.Context, token, id string) error { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return errors.Wrap(ErrUnauthorizedAccess, err) } @@ -311,20 +349,20 @@ func (ts *thingsService) RemoveChannel(ctx context.Context, token, id string) er return err } - return ts.channels.Remove(ctx, res.GetEmail(), id) + return ts.channels.Remove(ctx, res.GetId(), id) } func (ts *thingsService) Connect(ctx context.Context, token string, chIDs, thIDs []string) error { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return errors.Wrap(ErrUnauthorizedAccess, err) } - return ts.channels.Connect(ctx, res.GetEmail(), chIDs, thIDs) + return ts.channels.Connect(ctx, res.GetId(), chIDs, thIDs) } func (ts *thingsService) Disconnect(ctx context.Context, token string, chIDs, thIDs []string) error { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return errors.Wrap(ErrUnauthorizedAccess, err) } @@ -412,7 +450,7 @@ func (ts *thingsService) hasThing(ctx context.Context, chanID, thingKey string) } func (ts *thingsService) ListMembers(ctx context.Context, token, groupID string, pm PageMetadata) (Page, error) { - if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil { + if _, err := ts.identify(ctx, token); err != nil { return Page{}, errors.Wrap(ErrUnauthorizedAccess, err) } diff --git a/twins/service.go b/twins/service.go index 839cea18fd..03d7e22cf2 100644 --- a/twins/service.go +++ b/twins/service.go @@ -8,8 +8,10 @@ import ( "encoding/json" "fmt" "math" + "strings" "time" + "github.com/golang-jwt/jwt/v4" "github.com/mainflux/mainflux/logger" "github.com/mainflux/mainflux/pkg/errors" "github.com/mainflux/mainflux/pkg/messaging" @@ -97,12 +99,13 @@ type twinsService struct { channelID string twinCache TwinCache logger logger.Logger + OIDC bool } var _ Service = (*twinsService)(nil) // New instantiates the twins service implementation. -func New(publisher messaging.Publisher, auth mainflux.AuthServiceClient, twins TwinRepository, tcache TwinCache, sr StateRepository, idp mainflux.IDProvider, chann string, logger logger.Logger) Service { +func New(publisher messaging.Publisher, auth mainflux.AuthServiceClient, twins TwinRepository, tcache TwinCache, sr StateRepository, idp mainflux.IDProvider, chann string, oidc bool, logger logger.Logger) Service { return &twinsService{ publisher: publisher, auth: auth, @@ -112,6 +115,7 @@ func New(publisher messaging.Publisher, auth mainflux.AuthServiceClient, twins T idProvider: idp, channelID: chann, logger: logger, + OIDC: oidc, } } @@ -120,7 +124,7 @@ func (ts *twinsService) AddTwin(ctx context.Context, token string, twin Twin, de var b []byte defer ts.publish(&id, &err, crudOp["createSucc"], crudOp["createFail"], &b) - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return Twin{}, ErrUnauthorizedAccess } @@ -163,7 +167,7 @@ func (ts *twinsService) UpdateTwin(ctx context.Context, token string, twin Twin, var id string defer ts.publish(&id, &err, crudOp["updateSucc"], crudOp["updateFail"], &b) - _, err = ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + _, err = ts.identify(ctx, token) if err != nil { return ErrUnauthorizedAccess } @@ -213,7 +217,7 @@ func (ts *twinsService) ViewTwin(ctx context.Context, token, twinID string) (tw var b []byte defer ts.publish(&twinID, &err, crudOp["getSucc"], crudOp["getFail"], &b) - _, err = ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + _, err = ts.identify(ctx, token) if err != nil { return Twin{}, ErrUnauthorizedAccess } @@ -232,7 +236,7 @@ func (ts *twinsService) RemoveTwin(ctx context.Context, token, twinID string) (e var b []byte defer ts.publish(&twinID, &err, crudOp["removeSucc"], crudOp["removeFail"], &b) - _, err = ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + _, err = ts.identify(ctx, token) if err != nil { return ErrUnauthorizedAccess } @@ -245,7 +249,7 @@ func (ts *twinsService) RemoveTwin(ctx context.Context, token, twinID string) (e } func (ts *twinsService) ListTwins(ctx context.Context, token string, offset uint64, limit uint64, name string, metadata Metadata) (Page, error) { - res, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + res, err := ts.identify(ctx, token) if err != nil { return Page{}, ErrUnauthorizedAccess } @@ -254,7 +258,7 @@ func (ts *twinsService) ListTwins(ctx context.Context, token string, offset uint } func (ts *twinsService) ListStates(ctx context.Context, token string, offset uint64, limit uint64, twinID string) (StatesPage, error) { - _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}) + _, err := ts.identify(ctx, token) if err != nil { return StatesPage{}, ErrUnauthorizedAccess } @@ -293,6 +297,39 @@ func (ts *twinsService) SaveStates(msg *messaging.Message) error { return nil } +func (ts *twinsService) identify(ctx context.Context, token string) (u *mainflux.UserIdentity, err error) { + token = strings.ReplaceAll(token, "Bearer ", "") + if len(token) == 0 { + return nil, errors.Wrap(ErrUnauthorizedAccess, errors.New("Empty authorization token")) + } + + if ts.OIDC { + parsed, err := jwt.Parse(token, nil) + if err != nil { + fmt.Println(err) + } + if parsed == nil { + return nil, errors.Wrap(ErrUnauthorizedAccess, errors.New("Invalid authorization token")) + } + claims, _ := parsed.Claims.(jwt.MapClaims) + id, ok := claims["sub"].(string) + if !ok { + return nil, errors.Wrap(ErrUnauthorizedAccess, errors.New("Missing claim sub")) + } + email, ok := claims["email"].(string) + if !ok { + return nil, errors.Wrap(ErrUnauthorizedAccess, errors.New("Missing email in token")) + } + + return &mainflux.UserIdentity{ + Id: id, + Email: email, + }, nil + } + + return ts.auth.Identify(ctx, &mainflux.Token{Value: token}) +} + func (ts *twinsService) saveState(msg *messaging.Message, twinID string) error { var b []byte var err error diff --git a/users/api/endpoint_test.go b/users/api/endpoint_test.go index 777b21893c..4cd681aac1 100644 --- a/users/api/endpoint_test.go +++ b/users/api/endpoint_test.go @@ -78,7 +78,7 @@ func newService() users.Service { email := mocks.NewEmailer() idProvider := uuid.New() - return users.New(usersRepo, hasher, auth, email, idProvider, passRegex) + return users.New(usersRepo, hasher, auth, email, idProvider, false, passRegex) } func newServer(svc users.Service) *httptest.Server { diff --git a/users/service.go b/users/service.go index f7a0a9501b..7785406692 100644 --- a/users/service.go +++ b/users/service.go @@ -5,8 +5,11 @@ package users import ( "context" + "fmt" "regexp" + "strings" + "github.com/golang-jwt/jwt/v4" "github.com/mainflux/mainflux" "github.com/mainflux/mainflux/auth" "github.com/mainflux/mainflux/pkg/errors" @@ -125,17 +128,19 @@ type usersService struct { users UserRepository hasher Hasher email Emailer + oidc bool auth mainflux.AuthServiceClient idProvider mainflux.IDProvider passRegex *regexp.Regexp } // New instantiates the users service implementation -func New(users UserRepository, hasher Hasher, auth mainflux.AuthServiceClient, e Emailer, idp mainflux.IDProvider, passRegex *regexp.Regexp) Service { +func New(users UserRepository, hasher Hasher, auth mainflux.AuthServiceClient, e Emailer, idp mainflux.IDProvider, oidc bool, passRegex *regexp.Regexp) Service { return &usersService{ users: users, hasher: hasher, auth: auth, + oidc: oidc, email: e, idProvider: idp, passRegex: passRegex, @@ -197,19 +202,19 @@ func (svc usersService) ViewUser(ctx context.Context, token, id string) (User, e } func (svc usersService) ViewProfile(ctx context.Context, token string) (User, error) { - email, err := svc.identify(ctx, token) + u, err := svc.identify(ctx, token) if err != nil { return User{}, err } - dbUser, err := svc.users.RetrieveByEmail(ctx, email) + dbUser, err := svc.users.RetrieveByID(ctx, u.ID) if err != nil { return User{}, errors.Wrap(ErrUnauthorizedAccess, err) } return User{ ID: dbUser.ID, - Email: email, + Email: u.Email, Password: "", Metadata: dbUser.Metadata, }, nil @@ -225,12 +230,12 @@ func (svc usersService) ListUsers(ctx context.Context, token string, offset, lim } func (svc usersService) UpdateUser(ctx context.Context, token string, u User) error { - email, err := svc.identify(ctx, token) + us, err := svc.identify(ctx, token) if err != nil { return errors.Wrap(ErrUnauthorizedAccess, err) } user := User{ - Email: email, + Email: us.ID, Metadata: u.Metadata, } return svc.users.UpdateUser(ctx, user) @@ -249,11 +254,11 @@ func (svc usersService) GenerateResetToken(ctx context.Context, email, host stri } func (svc usersService) ResetPassword(ctx context.Context, resetToken, password string) error { - email, err := svc.identify(ctx, resetToken) + us, err := svc.identify(ctx, resetToken) if err != nil { return errors.Wrap(ErrUnauthorizedAccess, err) } - u, err := svc.users.RetrieveByEmail(ctx, email) + u, err := svc.users.RetrieveByID(ctx, us.ID) if err != nil || u.Email == "" { return ErrUserNotFound } @@ -264,11 +269,11 @@ func (svc usersService) ResetPassword(ctx context.Context, resetToken, password if err != nil { return err } - return svc.users.UpdatePassword(ctx, email, password) + return svc.users.UpdatePassword(ctx, us.Email, password) } func (svc usersService) ChangePassword(ctx context.Context, authToken, password, oldPassword string) error { - email, err := svc.identify(ctx, authToken) + us, err := svc.identify(ctx, authToken) if err != nil { return errors.Wrap(ErrUnauthorizedAccess, err) } @@ -276,13 +281,13 @@ func (svc usersService) ChangePassword(ctx context.Context, authToken, password, return ErrPasswordFormat } u := User{ - Email: email, + Email: us.Email, Password: oldPassword, } if _, err := svc.Login(ctx, u); err != nil { return ErrUnauthorizedAccess } - u, err = svc.users.RetrieveByEmail(ctx, email) + u, err = svc.users.RetrieveByEmail(ctx, us.Email) if err != nil || u.Email == "" { return ErrUserNotFound } @@ -291,7 +296,7 @@ func (svc usersService) ChangePassword(ctx context.Context, authToken, password, if err != nil { return err } - return svc.users.UpdatePassword(ctx, email, password) + return svc.users.UpdatePassword(ctx, us.Email, password) } func (svc usersService) SendPasswordReset(_ context.Context, host, email, token string) error { @@ -332,12 +337,53 @@ func (svc usersService) issue(ctx context.Context, id, email string, keyType uin return key.GetValue(), nil } -func (svc usersService) identify(ctx context.Context, token string) (string, error) { +func (svc usersService) identify(ctx context.Context, token string) (u User, err error) { + token = strings.ReplaceAll(token, "Bearer ", "") + if len(token) == 0 { + return User{}, errors.Wrap(ErrUnauthorizedAccess, errors.New("Empty authorization token")) + } + + if svc.oidc { + parsed, err := jwt.Parse(token, nil) + if err != nil { + fmt.Println(err) + } + if parsed == nil { + return User{}, errors.Wrap(ErrUnauthorizedAccess, errors.New("Invalid authorization token")) + } + + claims, _ := parsed.Claims.(jwt.MapClaims) + id, ok := claims["sub"].(string) + if !ok { + return User{}, errors.Wrap(ErrUnauthorizedAccess, errors.New("Missing claim sub")) + } + email, ok := claims["email"].(string) + if !ok { + return User{}, errors.Wrap(ErrUnauthorizedAccess, errors.New("Missing email in token")) + } + user := User{ + ID: id, + Email: email, + } + + if _, err := svc.users.RetrieveByID(ctx, user.ID); err != nil { + if _, err = svc.users.Save(ctx, user); err != nil { + return User{}, err + } + } + + return user, nil + } + identity, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token}) if err != nil { - return "", errors.Wrap(ErrUnauthorizedAccess, err) + return User{}, errors.Wrap(ErrUnauthorizedAccess, err) } - return identity.GetEmail(), nil + return User{ + ID: identity.GetId(), + Email: identity.GetEmail(), + }, nil + } func (svc usersService) members(ctx context.Context, token, groupID string, limit, offset uint64) ([]string, error) { diff --git a/users/service_test.go b/users/service_test.go index bcf6d25d49..919c3e1f3a 100644 --- a/users/service_test.go +++ b/users/service_test.go @@ -36,7 +36,7 @@ func newService() users.Service { auth := mocks.NewAuthService(map[string]string{user.Email: user.Email}) e := mocks.NewEmailer() - return users.New(userRepo, hasher, auth, e, idProvider, passRegex) + return users.New(userRepo, hasher, auth, e, idProvider, false, passRegex) } func TestRegister(t *testing.T) {