Repository: clouditor/clouditor
Pull request #520 (2022-02-16 15:22:02)

Support OAuth 2.0
This PR adds an OAuth 2.0 compliant authorizer for services. Furthermore, it removes our own implementation of an authorisation server and embeds a small OAuth 2.0 auth server from an external package.

oxisto: Addressed all the review comments @garuppel.

Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 75621fad..881d3f5c 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -21,7 +21,51 @@
             "mode": "auto",
             "program": "${workspaceFolder}/cmd/engine/engine.go",
             "cwd": "${workspaceFolder}",
-            "args": ["--db-in-memory"],
+            "args": [
+                "--db-in-memory"
+            ],
+            "env": {},
+            "preLaunchTask": "go: generate"
+        },
+        {
+            "name": "Launch engine (dashboard development redirect)",
+            "type": "go",
+            "request": "launch",
+            "mode": "auto",
+            "program": "${workspaceFolder}/cmd/engine/engine.go",
+            "cwd": "${workspaceFolder}",
+            "args": [
+                "--db-in-memory",
+                "--dashboard-url=http://localhost:3000"
+            ],
+            "env": {},
+            "preLaunchTask": "go: generate"
+        },
+        {
+            "name": "Launch cli (cloud list)",
+            "type": "go",
+            "request": "launch",
+            "mode": "auto",
+            "program": "${workspaceFolder}/cmd/cli/cl.go",
+            "cwd": "${workspaceFolder}",
+            "args": [
+                "cloud",
+                "list"
+            ],
+            "env": {},
+            "preLaunchTask": "go: generate"
+        },
+        {
+            "name": "Launch cli (login)",
+            "type": "go",
+            "request": "launch",
+            "mode": "auto",
+            "program": "${workspaceFolder}/cmd/cli/cl.go",
+            "cwd": "${workspaceFolder}",
+            "args": [
+                "login",
+                "localhost:9090"
+            ],
             "env": {},
             "preLaunchTask": "go: generate"
         }
diff --git a/.vscode/settings.json b/.vscode/settings.json
index fd99e282..463347e9 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -17,6 +17,8 @@
         "gorm",
         "Infof",
         "inmemory",
+        "jwks",
+        "JWKSURL",
         "Kube",
         "Kubernetes",
         "oxisto",
diff --git a/api/assessment/assessment.pb.gw.go b/api/assessment/assessment.pb.gw.go
index 5998997e..2f84db31 100644
--- a/api/assessment/assessment.pb.gw.go
+++ b/api/assessment/assessment.pb.gw.go
@@ -95,7 +95,7 @@ func RegisterAssessmentHandlerServer(ctx context.Context, mux *runtime.ServeMux,
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Assessment/AssessEvidence")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Assessment/AssessEvidence", runtime.WithHTTPPathPattern("/v1/assessment/evidences"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -118,7 +118,7 @@ func RegisterAssessmentHandlerServer(ctx context.Context, mux *runtime.ServeMux,
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Assessment/ListAssessmentResults")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Assessment/ListAssessmentResults", runtime.WithHTTPPathPattern("/v1/assessment/results"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -165,7 +165,7 @@ func RegisterAssessmentHandlerFromEndpoint(ctx context.Context, mux *runtime.Ser
 
 // RegisterAssessmentHandler registers the http handlers for service Assessment to "mux".
 // The handlers forward requests to the grpc endpoint over "conn".
-func RegisterAssessmentHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
+func RegisterAssessmentHandler(ctx context.Context, mux *runtime.ServeMux, conn grpc.ClientConnInterface) error {
 	return RegisterAssessmentHandlerClient(ctx, mux, NewAssessmentClient(conn))
 }
 
@@ -180,7 +180,7 @@ func RegisterAssessmentHandlerClient(ctx context.Context, mux *runtime.ServeMux,
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Assessment/AssessEvidence")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Assessment/AssessEvidence", runtime.WithHTTPPathPattern("/v1/assessment/evidences"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -200,7 +200,7 @@ func RegisterAssessmentHandlerClient(ctx context.Context, mux *runtime.ServeMux,
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Assessment/ListAssessmentResults")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Assessment/ListAssessmentResults", runtime.WithHTTPPathPattern("/v1/assessment/results"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
diff --git a/api/auth/auth.pb.go b/api/auth/auth.pb.go
index 05b75059..bf3331ae 100644
--- a/api/auth/auth.pb.go
+++ b/api/auth/auth.pb.go
@@ -33,10 +33,8 @@
 package auth
 
 import (
-	_ "google.golang.org/genproto/googleapis/api/annotations"
 	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
 	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
-	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
 	reflect "reflect"
 	sync "sync"
 )
@@ -128,467 +126,21 @@ func (x *User) GetShadow() bool {
 	return false
 }
 
-type LoginRequest struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
-	Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
-}
-
-func (x *LoginRequest) Reset() {
-	*x = LoginRequest{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_api_auth_auth_proto_msgTypes[1]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *LoginRequest) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*LoginRequest) ProtoMessage() {}
-
-func (x *LoginRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_api_auth_auth_proto_msgTypes[1]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead.
-func (*LoginRequest) Descriptor() ([]byte, []int) {
-	return file_api_auth_auth_proto_rawDescGZIP(), []int{1}
-}
-
-func (x *LoginRequest) GetUsername() string {
-	if x != nil {
-		return x.Username
-	}
-	return ""
-}
-
-func (x *LoginRequest) GetPassword() string {
-	if x != nil {
-		return x.Password
-	}
-	return ""
-}
-
-// LoginResponse is a response to a Login call, it is aligned to a OAuth 2.0
-// token response (see
-// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4).
-type TokenResponse struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	// access_token contains the token which can be used to authenticate further
-	// calls to a resource endpoint
-	AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
-	// token_type describes the access token type. The usual value in most
-	// scenarios is "bearer".
-	TokenType string `protobuf:"bytes,2,opt,name=token_type,json=tokenType,proto3" json:"token_type,omitempty"`
-	// refresh_token is a token that can be used to refresh the access token if
-	// it expires. Reserved for future use, not currently used.
-	RefreshToken string `protobuf:"bytes,3,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
-	// expiry is an optional expiration time of the access token.
-	Expiry *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiry,proto3" json:"expiry,omitempty"`
-}
-
-func (x *TokenResponse) Reset() {
-	*x = TokenResponse{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_api_auth_auth_proto_msgTypes[2]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *TokenResponse) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*TokenResponse) ProtoMessage() {}
-
-func (x *TokenResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_api_auth_auth_proto_msgTypes[2]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
-func (*TokenResponse) Descriptor() ([]byte, []int) {
-	return file_api_auth_auth_proto_rawDescGZIP(), []int{2}
-}
-
-func (x *TokenResponse) GetAccessToken() string {
-	if x != nil {
-		return x.AccessToken
-	}
-	return ""
-}
-
-func (x *TokenResponse) GetTokenType() string {
-	if x != nil {
-		return x.TokenType
-	}
-	return ""
-}
-
-func (x *TokenResponse) GetRefreshToken() string {
-	if x != nil {
-		return x.RefreshToken
-	}
-	return ""
-}
-
-func (x *TokenResponse) GetExpiry() *timestamppb.Timestamp {
-	if x != nil {
-		return x.Expiry
-	}
-	return nil
-}
-
-// TokenRequest is a data structure that contains all necessary request
-// parameters for an OAuth 2.0 access token request (see
-// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3).
-type TokenRequest struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	GrantType    string `protobuf:"bytes,1,opt,name=grant_type,json=grantType,proto3" json:"grant_type,omitempty"`
-	RefreshToken string `protobuf:"bytes,2,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
-	Scope        string `protobuf:"bytes,3,opt,name=scope,proto3" json:"scope,omitempty"`
-	ClientId     string `protobuf:"bytes,4,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
-}
-
-func (x *TokenRequest) Reset() {
-	*x = TokenRequest{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_api_auth_auth_proto_msgTypes[3]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *TokenRequest) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*TokenRequest) ProtoMessage() {}
-
-func (x *TokenRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_api_auth_auth_proto_msgTypes[3]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use TokenRequest.ProtoReflect.Descriptor instead.
-func (*TokenRequest) Descriptor() ([]byte, []int) {
-	return file_api_auth_auth_proto_rawDescGZIP(), []int{3}
-}
-
-func (x *TokenRequest) GetGrantType() string {
-	if x != nil {
-		return x.GrantType
-	}
-	return ""
-}
-
-func (x *TokenRequest) GetRefreshToken() string {
-	if x != nil {
-		return x.RefreshToken
-	}
-	return ""
-}
-
-func (x *TokenRequest) GetScope() string {
-	if x != nil {
-		return x.Scope
-	}
-	return ""
-}
-
-func (x *TokenRequest) GetClientId() string {
-	if x != nil {
-		return x.ClientId
-	}
-	return ""
-}
-
-type ListPublicKeysRequest struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-}
-
-func (x *ListPublicKeysRequest) Reset() {
-	*x = ListPublicKeysRequest{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_api_auth_auth_proto_msgTypes[4]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *ListPublicKeysRequest) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*ListPublicKeysRequest) ProtoMessage() {}
-
-func (x *ListPublicKeysRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_api_auth_auth_proto_msgTypes[4]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use ListPublicKeysRequest.ProtoReflect.Descriptor instead.
-func (*ListPublicKeysRequest) Descriptor() ([]byte, []int) {
-	return file_api_auth_auth_proto_rawDescGZIP(), []int{4}
-}
-
-type ListPublicResponse struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	Keys []*JsonWebKey `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"`
-}
-
-func (x *ListPublicResponse) Reset() {
-	*x = ListPublicResponse{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_api_auth_auth_proto_msgTypes[5]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *ListPublicResponse) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*ListPublicResponse) ProtoMessage() {}
-
-func (x *ListPublicResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_api_auth_auth_proto_msgTypes[5]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use ListPublicResponse.ProtoReflect.Descriptor instead.
-func (*ListPublicResponse) Descriptor() ([]byte, []int) {
-	return file_api_auth_auth_proto_rawDescGZIP(), []int{5}
-}
-
-func (x *ListPublicResponse) GetKeys() []*JsonWebKey {
-	if x != nil {
-		return x.Keys
-	}
-	return nil
-}
-
-// JsonWebKey describes a JSON Web Key (JWK) according to RFC 7517 (see
-// https://datatracker.ietf.org/doc/html/rfc7517) and RFC 7518
-// (https://datatracker.ietf.org/doc/html/rfc7518).
-type JsonWebKey struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	// kid is the Key ID Parameter (see
-	// https://datatracker.ietf.org/doc/html/rfc7517#section-4.5).
-	Kid string `protobuf:"bytes,1,opt,name=kid,proto3" json:"kid,omitempty"`
-	// kty is the Key Type Parameter (see
-	// https://datatracker.ietf.org/doc/html/rfc7517#section-4.1).
-	Kty string `protobuf:"bytes,22,opt,name=kty,proto3" json:"kty,omitempty"`
-	// crv is the Curve Parameter when using an elliptic curve public key in a JWK
-	// (see https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.1).
-	Crv string `protobuf:"bytes,10,opt,name=crv,proto3" json:"crv,omitempty"`
-	// x is the X Coordinate Parameter when using an elliptic curve public key in
-	// a JWK (see https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.2).
-	X string `protobuf:"bytes,11,opt,name=x,proto3" json:"x,omitempty"`
-	// y is the Y Coordinate Parameter when using an elliptic curve public key in
-	// a JWK (see https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3).
-	Y string `protobuf:"bytes,12,opt,name=y,proto3" json:"y,omitempty"`
-}
-
-func (x *JsonWebKey) Reset() {
-	*x = JsonWebKey{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_api_auth_auth_proto_msgTypes[6]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *JsonWebKey) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*JsonWebKey) ProtoMessage() {}
-
-func (x *JsonWebKey) ProtoReflect() protoreflect.Message {
-	mi := &file_api_auth_auth_proto_msgTypes[6]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use JsonWebKey.ProtoReflect.Descriptor instead.
-func (*JsonWebKey) Descriptor() ([]byte, []int) {
-	return file_api_auth_auth_proto_rawDescGZIP(), []int{6}
-}
-
-func (x *JsonWebKey) GetKid() string {
-	if x != nil {
-		return x.Kid
-	}
-	return ""
-}
-
-func (x *JsonWebKey) GetKty() string {
-	if x != nil {
-		return x.Kty
-	}
-	return ""
-}
-
-func (x *JsonWebKey) GetCrv() string {
-	if x != nil {
-		return x.Crv
-	}
-	return ""
-}
-
-func (x *JsonWebKey) GetX() string {
-	if x != nil {
-		return x.X
-	}
-	return ""
-}
-
-func (x *JsonWebKey) GetY() string {
-	if x != nil {
-		return x.Y
-	}
-	return ""
-}
-
 var File_api_auth_auth_proto protoreflect.FileDescriptor
 
 var file_api_auth_auth_proto_rawDesc = []byte{
 	0x0a, 0x13, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2e,
 	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x69, 0x74, 0x6f, 0x72,
-	0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
-	0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e,
-	0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
-	0x89, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72,
-	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72,
-	0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
-	0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e,
-	0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x4e,
-	0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x18, 0x05, 0x20,
-	0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x22, 0x46, 0x0a, 0x0c, 0x4c,
-	0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75,
-	0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75,
-	0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77,
-	0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77,
-	0x6f, 0x72, 0x64, 0x22, 0xaa, 0x01, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73,
-	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f,
-	0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63,
-	0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65,
-	0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f,
-	0x6b, 0x65, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65,
-	0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,
-	0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x32, 0x0a, 0x06,
-	0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67,
-	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54,
-	0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79,
-	0x22, 0x85, 0x01, 0x0a, 0x0c, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
-	0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65,
-	0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65,
-	0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68,
-	0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x03,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63,
-	0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
-	0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x17, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74,
-	0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
-	0x74, 0x22, 0x3f, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x52,
-	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18,
-	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x69, 0x74, 0x6f,
-	0x72, 0x2e, 0x4a, 0x73, 0x6f, 0x6e, 0x57, 0x65, 0x62, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65,
-	0x79, 0x73, 0x22, 0x5e, 0x0a, 0x0a, 0x4a, 0x73, 0x6f, 0x6e, 0x57, 0x65, 0x62, 0x4b, 0x65, 0x79,
-	0x12, 0x10, 0x0a, 0x03, 0x6b, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
-	0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x74, 0x79, 0x18, 0x16, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x03, 0x6b, 0x74, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x72, 0x76, 0x18, 0x0a, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x03, 0x63, 0x72, 0x76, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x0b, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x01, 0x79, 0x32, 0xb4, 0x02, 0x0a, 0x0e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63,
-	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x58, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x17,
-	0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x69,
-	0x74, 0x6f, 0x72, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
-	0x65, 0x22, 0x1c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x16, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x61,
-	0x75, 0x74, 0x68, 0x2f, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x3a, 0x01, 0x2a, 0x62, 0x01, 0x2a, 0x12,
-	0x52, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x17, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
-	0x69, 0x74, 0x6f, 0x72, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
-	0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x69, 0x74, 0x6f, 0x72, 0x2e, 0x54, 0x6f,
-	0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4,
-	0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x74, 0x6f,
-	0x6b, 0x65, 0x6e, 0x12, 0x74, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69,
-	0x63, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x20, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x69, 0x74, 0x6f,
-	0x72, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x73,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x69,
-	0x74, 0x6f, 0x72, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x21, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x12, 0x16,
-	0x2f, 0x2e, 0x77, 0x65, 0x6c, 0x6c, 0x2d, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x2f, 0x6a, 0x77, 0x6b,
-	0x73, 0x2e, 0x6a, 0x73, 0x6f, 0x6e, 0x62, 0x01, 0x2a, 0x42, 0x0a, 0x5a, 0x08, 0x61, 0x70, 0x69,
-	0x2f, 0x61, 0x75, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x22, 0x89, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65,
+	0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65,
+	0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72,
+	0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72,
+	0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f,
+	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c,
+	0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x18, 0x05,
+	0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x42, 0x0a, 0x5a, 0x08,
+	0x61, 0x70, 0x69, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -603,31 +155,16 @@ func file_api_auth_auth_proto_rawDescGZIP() []byte {
 	return file_api_auth_auth_proto_rawDescData
 }
 
-var file_api_auth_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
+var file_api_auth_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
 var file_api_auth_auth_proto_goTypes = []interface{}{
-	(*User)(nil),                  // 0: clouditor.User
-	(*LoginRequest)(nil),          // 1: clouditor.LoginRequest
-	(*TokenResponse)(nil),         // 2: clouditor.TokenResponse
-	(*TokenRequest)(nil),          // 3: clouditor.TokenRequest
-	(*ListPublicKeysRequest)(nil), // 4: clouditor.ListPublicKeysRequest
-	(*ListPublicResponse)(nil),    // 5: clouditor.ListPublicResponse
-	(*JsonWebKey)(nil),            // 6: clouditor.JsonWebKey
-	(*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp
+	(*User)(nil), // 0: clouditor.User
 }
 var file_api_auth_auth_proto_depIdxs = []int32{
-	7, // 0: clouditor.TokenResponse.expiry:type_name -> google.protobuf.Timestamp
-	6, // 1: clouditor.ListPublicResponse.keys:type_name -> clouditor.JsonWebKey
-	1, // 2: clouditor.Authentication.Login:input_type -> clouditor.LoginRequest
-	3, // 3: clouditor.Authentication.Token:input_type -> clouditor.TokenRequest
-	4, // 4: clouditor.Authentication.ListPublicKeys:input_type -> clouditor.ListPublicKeysRequest
-	2, // 5: clouditor.Authentication.Login:output_type -> clouditor.TokenResponse
-	2, // 6: clouditor.Authentication.Token:output_type -> clouditor.TokenResponse
-	5, // 7: clouditor.Authentication.ListPublicKeys:output_type -> clouditor.ListPublicResponse
-	5, // [5:8] is the sub-list for method output_type
-	2, // [2:5] is the sub-list for method input_type
-	2, // [2:2] is the sub-list for extension type_name
-	2, // [2:2] is the sub-list for extension extendee
-	0, // [0:2] is the sub-list for field type_name
+	0, // [0:0] is the sub-list for method output_type
+	0, // [0:0] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
 }
 
 func init() { file_api_auth_auth_proto_init() }
@@ -648,78 +185,6 @@ func file_api_auth_auth_proto_init() {
 				return nil
 			}
 		}
-		file_api_auth_auth_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*LoginRequest); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-		file_api_auth_auth_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*TokenResponse); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-		file_api_auth_auth_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*TokenRequest); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-		file_api_auth_auth_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*ListPublicKeysRequest); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-		file_api_auth_auth_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*ListPublicResponse); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-		file_api_auth_auth_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*JsonWebKey); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
 	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
@@ -727,9 +192,9 @@ func file_api_auth_auth_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_api_auth_auth_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   7,
+			NumMessages:   1,
 			NumExtensions: 0,
-			NumServices:   1,
+			NumServices:   0,
 		},
 		GoTypes:           file_api_auth_auth_proto_goTypes,
 		DependencyIndexes: file_api_auth_auth_proto_depIdxs,
diff --git a/api/auth/auth.pb.gw.go b/api/auth/auth.pb.gw.go
deleted file mode 100644
index 18662780..00000000
--- a/api/auth/auth.pb.gw.go
+++ /dev/null
@@ -1,315 +0,0 @@
-// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
-// source: api/auth/auth.proto
-
-/*
-Package auth is a reverse proxy.
-
-It translates gRPC into RESTful JSON APIs.
-*/
-package auth
-
-import (
-	"context"
-	"io"
-	"net/http"
-
-	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
-	"github.com/grpc-ecosystem/grpc-gateway/v2/utilities"
-	"google.golang.org/grpc"
-	"google.golang.org/grpc/codes"
-	"google.golang.org/grpc/grpclog"
-	"google.golang.org/grpc/metadata"
-	"google.golang.org/grpc/status"
-	"google.golang.org/protobuf/proto"
-)
-
-// Suppress "imported and not used" errors
-var _ codes.Code
-var _ io.Reader
-var _ status.Status
-var _ = runtime.String
-var _ = utilities.NewDoubleArray
-var _ = metadata.Join
-
-func request_Authentication_Login_0(ctx context.Context, marshaler runtime.Marshaler, client AuthenticationClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var protoReq LoginRequest
-	var metadata runtime.ServerMetadata
-
-	newReader, berr := utilities.IOReaderFactory(req.Body)
-	if berr != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
-	}
-	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-
-	msg, err := client.Login(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-
-}
-
-func local_request_Authentication_Login_0(ctx context.Context, marshaler runtime.Marshaler, server AuthenticationServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var protoReq LoginRequest
-	var metadata runtime.ServerMetadata
-
-	newReader, berr := utilities.IOReaderFactory(req.Body)
-	if berr != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
-	}
-	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-
-	msg, err := server.Login(ctx, &protoReq)
-	return msg, metadata, err
-
-}
-
-var (
-	filter_Authentication_Token_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
-)
-
-func request_Authentication_Token_0(ctx context.Context, marshaler runtime.Marshaler, client AuthenticationClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var protoReq TokenRequest
-	var metadata runtime.ServerMetadata
-
-	if err := req.ParseForm(); err != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Authentication_Token_0); err != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-
-	msg, err := client.Token(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-
-}
-
-func local_request_Authentication_Token_0(ctx context.Context, marshaler runtime.Marshaler, server AuthenticationServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var protoReq TokenRequest
-	var metadata runtime.ServerMetadata
-
-	if err := req.ParseForm(); err != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-	if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Authentication_Token_0); err != nil {
-		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
-	}
-
-	msg, err := server.Token(ctx, &protoReq)
-	return msg, metadata, err
-
-}
-
-func request_Authentication_ListPublicKeys_0(ctx context.Context, marshaler runtime.Marshaler, client AuthenticationClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var protoReq ListPublicKeysRequest
-	var metadata runtime.ServerMetadata
-
-	msg, err := client.ListPublicKeys(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
-	return msg, metadata, err
-
-}
-
-func local_request_Authentication_ListPublicKeys_0(ctx context.Context, marshaler runtime.Marshaler, server AuthenticationServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
-	var protoReq ListPublicKeysRequest
-	var metadata runtime.ServerMetadata
-
-	msg, err := server.ListPublicKeys(ctx, &protoReq)
-	return msg, metadata, err
-
-}
-
-// RegisterAuthenticationHandlerServer registers the http handlers for service Authentication to "mux".
-// UnaryRPC     :call AuthenticationServer directly.
-// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
-// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAuthenticationHandlerFromEndpoint instead.
-func RegisterAuthenticationHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthenticationServer) error {
-
-	mux.Handle("POST", pattern_Authentication_Login_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Authentication/Login")
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_Authentication_Login_0(rctx, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		ctx = runtime.NewServerMetadataContext(ctx, md)
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-
-		forward_Authentication_Login_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-
-	})
-
-	mux.Handle("GET", pattern_Authentication_Token_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Authentication/Token")
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_Authentication_Token_0(rctx, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		ctx = runtime.NewServerMetadataContext(ctx, md)
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-
-		forward_Authentication_Token_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-
-	})
-
-	mux.Handle("GET", pattern_Authentication_ListPublicKeys_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		var stream runtime.ServerTransportStream
-		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Authentication/ListPublicKeys")
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := local_request_Authentication_ListPublicKeys_0(rctx, inboundMarshaler, server, req, pathParams)
-		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
-		ctx = runtime.NewServerMetadataContext(ctx, md)
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-
-		forward_Authentication_ListPublicKeys_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-
-	})
-
-	return nil
-}
-
-// RegisterAuthenticationHandlerFromEndpoint is same as RegisterAuthenticationHandler but
-// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
-func RegisterAuthenticationHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
-	conn, err := grpc.Dial(endpoint, opts...)
-	if err != nil {
-		return err
-	}
-	defer func() {
-		if err != nil {
-			if cerr := conn.Close(); cerr != nil {
-				grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
-			}
-			return
-		}
-		go func() {
-			<-ctx.Done()
-			if cerr := conn.Close(); cerr != nil {
-				grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
-			}
-		}()
-	}()
-
-	return RegisterAuthenticationHandler(ctx, mux, conn)
-}
-
-// RegisterAuthenticationHandler registers the http handlers for service Authentication to "mux".
-// The handlers forward requests to the grpc endpoint over "conn".
-func RegisterAuthenticationHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
-	return RegisterAuthenticationHandlerClient(ctx, mux, NewAuthenticationClient(conn))
-}
-
-// RegisterAuthenticationHandlerClient registers the http handlers for service Authentication
-// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AuthenticationClient".
-// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AuthenticationClient"
-// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
-// "AuthenticationClient" to call the correct interceptors.
-func RegisterAuthenticationHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AuthenticationClient) error {
-
-	mux.Handle("POST", pattern_Authentication_Login_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Authentication/Login")
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_Authentication_Login_0(rctx, inboundMarshaler, client, req, pathParams)
-		ctx = runtime.NewServerMetadataContext(ctx, md)
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-
-		forward_Authentication_Login_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-
-	})
-
-	mux.Handle("GET", pattern_Authentication_Token_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Authentication/Token")
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_Authentication_Token_0(rctx, inboundMarshaler, client, req, pathParams)
-		ctx = runtime.NewServerMetadataContext(ctx, md)
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-
-		forward_Authentication_Token_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-
-	})
-
-	mux.Handle("GET", pattern_Authentication_ListPublicKeys_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
-		ctx, cancel := context.WithCancel(req.Context())
-		defer cancel()
-		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Authentication/ListPublicKeys")
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-		resp, md, err := request_Authentication_ListPublicKeys_0(rctx, inboundMarshaler, client, req, pathParams)
-		ctx = runtime.NewServerMetadataContext(ctx, md)
-		if err != nil {
-			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
-			return
-		}
-
-		forward_Authentication_ListPublicKeys_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
-
-	})
-
-	return nil
-}
-
-var (
-	pattern_Authentication_Login_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "auth", "login"}, ""))
-
-	pattern_Authentication_Token_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "auth", "token"}, ""))
-
-	pattern_Authentication_ListPublicKeys_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{".well-known", "jwks.json"}, ""))
-)
-
-var (
-	forward_Authentication_Login_0 = runtime.ForwardResponseMessage
-
-	forward_Authentication_Token_0 = runtime.ForwardResponseMessage
-
-	forward_Authentication_ListPublicKeys_0 = runtime.ForwardResponseMessage
-)
diff --git a/api/auth/auth.proto b/api/auth/auth.proto
index 49b9a00d..cf64b8ee 100644
--- a/api/auth/auth.proto
+++ b/api/auth/auth.proto
@@ -28,41 +28,8 @@ syntax = "proto3";
 
 package clouditor;
 
-import "google/protobuf/timestamp.proto";
-import "google/api/annotations.proto";
-
 option go_package = "api/auth";
 
-service Authentication {
-  rpc Login(LoginRequest) returns (TokenResponse) {
-    option (google.api.http) = {
-      post : "/v1/auth/login"
-      // TODO: Here, "*" is good since username and password (fields of
-      // LoginRequest) will be the corresponding fields, right?
-      body : "*"
-      response_body : "*"
-    };
-  };
-
-  // GetToken is an OAuth 2.0 compliant token endpoint
-  rpc Token(TokenRequest) returns (TokenResponse) {
-    option (google.api.http) = {
-      // this needs to be a GET request in order for grpc-gateway to map
-      // application/x-www-form-urlencoded form params into the request
-      get : "/v1/auth/token"
-    };
-  };
-
-  // ListPublicKeys lists all available public keys of the authentication
-  // server in the JSON Web Key Set (JWKS) format.
-  rpc ListPublicKeys(ListPublicKeysRequest) returns (ListPublicResponse) {
-    option (google.api.http) = {
-      get : "/.well-known/jwks.json"
-      response_body : "*"
-    };
-  }
-}
-
 // A clouditor user
 message User {
   string username = 1;
@@ -71,69 +38,3 @@ message User {
   string full_name = 4;
   bool shadow = 5;
 }
-
-message LoginRequest {
-  string username = 1;
-  string password = 2;
-}
-
-// LoginResponse is a response to a Login call, it is aligned to a OAuth 2.0
-// token response (see
-// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4).
-message TokenResponse {
-  // access_token contains the token which can be used to authenticate further
-  // calls to a resource endpoint
-  string access_token = 1;
-
-  // token_type describes the access token type. The usual value in most
-  // scenarios is "bearer".
-  string token_type = 2;
-
-  // refresh_token is a token that can be used to refresh the access token if
-  // it expires. Reserved for future use, not currently used.
-  string refresh_token = 3;
-
-  // expiry is an optional expiration time of the access token.
-  google.protobuf.Timestamp expiry = 4;
-}
-
-// TokenRequest is a data structure that contains all necessary request
-// parameters for an OAuth 2.0 access token request (see
-// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3).
-message TokenRequest {
-  string grant_type = 1;
-
-  string refresh_token = 2;
-
-  string scope = 3;
-
-  string client_id = 4;
-}
-
-message ListPublicKeysRequest {}
-message ListPublicResponse { repeated JsonWebKey keys = 1; }
-
-// JsonWebKey describes a JSON Web Key (JWK) according to RFC 7517 (see
-// https://datatracker.ietf.org/doc/html/rfc7517) and RFC 7518
-// (https://datatracker.ietf.org/doc/html/rfc7518).
-message JsonWebKey {
-  // kid is the Key ID Parameter (see
-  // https://datatracker.ietf.org/doc/html/rfc7517#section-4.5).
-  string kid = 1;
-
-  // kty is the Key Type Parameter (see
-  // https://datatracker.ietf.org/doc/html/rfc7517#section-4.1).
-  string kty = 22;
-
-  // crv is the Curve Parameter when using an elliptic curve public key in a JWK
-  // (see https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.1).
-  string crv = 10;
-
-  // x is the X Coordinate Parameter when using an elliptic curve public key in
-  // a JWK (see https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.2).
-  string x = 11;
-
-  // y is the Y Coordinate Parameter when using an elliptic curve public key in
-  // a JWK (see https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3).
-  string y = 12;
-}
\ No newline at end of file
diff --git a/api/auth/auth_grpc.pb.go b/api/auth/auth_grpc.pb.go
deleted file mode 100644
index d1155b2b..00000000
--- a/api/auth/auth_grpc.pb.go
+++ /dev/null
@@ -1,183 +0,0 @@
-// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
-// versions:
-// - protoc-gen-go-grpc v1.2.0
-// - protoc             v3.19.4
-// source: api/auth/auth.proto
-
-package auth
-
-import (
-	context "context"
-	grpc "google.golang.org/grpc"
-	codes "google.golang.org/grpc/codes"
-	status "google.golang.org/grpc/status"
-)
-
-// This is a compile-time assertion to ensure that this generated file
-// is compatible with the grpc package it is being compiled against.
-// Requires gRPC-Go v1.32.0 or later.
-const _ = grpc.SupportPackageIsVersion7
-
-// AuthenticationClient is the client API for Authentication service.
-//
-// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
-type AuthenticationClient interface {
-	Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*TokenResponse, error)
-	// GetToken is an OAuth 2.0 compliant token endpoint
-	Token(ctx context.Context, in *TokenRequest, opts ...grpc.CallOption) (*TokenResponse, error)
-	// ListPublicKeys lists all available public keys of the authentication
-	// server in the JSON Web Key Set (JWKS) format.
-	ListPublicKeys(ctx context.Context, in *ListPublicKeysRequest, opts ...grpc.CallOption) (*ListPublicResponse, error)
-}
-
-type authenticationClient struct {
-	cc grpc.ClientConnInterface
-}
-
-func NewAuthenticationClient(cc grpc.ClientConnInterface) AuthenticationClient {
-	return &authenticationClient{cc}
-}
-
-func (c *authenticationClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*TokenResponse, error) {
-	out := new(TokenResponse)
-	err := c.cc.Invoke(ctx, "/clouditor.Authentication/Login", in, out, opts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *authenticationClient) Token(ctx context.Context, in *TokenRequest, opts ...grpc.CallOption) (*TokenResponse, error) {
-	out := new(TokenResponse)
-	err := c.cc.Invoke(ctx, "/clouditor.Authentication/Token", in, out, opts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-func (c *authenticationClient) ListPublicKeys(ctx context.Context, in *ListPublicKeysRequest, opts ...grpc.CallOption) (*ListPublicResponse, error) {
-	out := new(ListPublicResponse)
-	err := c.cc.Invoke(ctx, "/clouditor.Authentication/ListPublicKeys", in, out, opts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-// AuthenticationServer is the server API for Authentication service.
-// All implementations must embed UnimplementedAuthenticationServer
-// for forward compatibility
-type AuthenticationServer interface {
-	Login(context.Context, *LoginRequest) (*TokenResponse, error)
-	// GetToken is an OAuth 2.0 compliant token endpoint
-	Token(context.Context, *TokenRequest) (*TokenResponse, error)
-	// ListPublicKeys lists all available public keys of the authentication
-	// server in the JSON Web Key Set (JWKS) format.
-	ListPublicKeys(context.Context, *ListPublicKeysRequest) (*ListPublicResponse, error)
-	mustEmbedUnimplementedAuthenticationServer()
-}
-
-// UnimplementedAuthenticationServer must be embedded to have forward compatible implementations.
-type UnimplementedAuthenticationServer struct {
-}
-
-func (UnimplementedAuthenticationServer) Login(context.Context, *LoginRequest) (*TokenResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
-}
-func (UnimplementedAuthenticationServer) Token(context.Context, *TokenRequest) (*TokenResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method Token not implemented")
-}
-func (UnimplementedAuthenticationServer) ListPublicKeys(context.Context, *ListPublicKeysRequest) (*ListPublicResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method ListPublicKeys not implemented")
-}
-func (UnimplementedAuthenticationServer) mustEmbedUnimplementedAuthenticationServer() {}
-
-// UnsafeAuthenticationServer may be embedded to opt out of forward compatibility for this service.
-// Use of this interface is not recommended, as added methods to AuthenticationServer will
-// result in compilation errors.
-type UnsafeAuthenticationServer interface {
-	mustEmbedUnimplementedAuthenticationServer()
-}
-
-func RegisterAuthenticationServer(s grpc.ServiceRegistrar, srv AuthenticationServer) {
-	s.RegisterService(&Authentication_ServiceDesc, srv)
-}
-
-func _Authentication_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(LoginRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(AuthenticationServer).Login(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: "/clouditor.Authentication/Login",
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(AuthenticationServer).Login(ctx, req.(*LoginRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _Authentication_Token_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(TokenRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(AuthenticationServer).Token(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: "/clouditor.Authentication/Token",
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(AuthenticationServer).Token(ctx, req.(*TokenRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-func _Authentication_ListPublicKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(ListPublicKeysRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(AuthenticationServer).ListPublicKeys(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: "/clouditor.Authentication/ListPublicKeys",
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(AuthenticationServer).ListPublicKeys(ctx, req.(*ListPublicKeysRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-// Authentication_ServiceDesc is the grpc.ServiceDesc for Authentication service.
-// It's only intended for direct use with grpc.RegisterService,
-// and not to be introspected or modified (even as a copy)
-var Authentication_ServiceDesc = grpc.ServiceDesc{
-	ServiceName: "clouditor.Authentication",
-	HandlerType: (*AuthenticationServer)(nil),
-	Methods: []grpc.MethodDesc{
-		{
-			MethodName: "Login",
-			Handler:    _Authentication_Login_Handler,
-		},
-		{
-			MethodName: "Token",
-			Handler:    _Authentication_Token_Handler,
-		},
-		{
-			MethodName: "ListPublicKeys",
-			Handler:    _Authentication_ListPublicKeys_Handler,
-		},
-	},
-	Streams:  []grpc.StreamDesc{},
-	Metadata: "api/auth/auth.proto",
-}
diff --git a/api/authorizer.go b/api/authorizer.go
index 3bdb1092..bc01324c 100644
--- a/api/authorizer.go
+++ b/api/authorizer.go
@@ -28,11 +28,9 @@ package api
 import (
 	"context"
 	"fmt"
-	"sync"
-	"time"
 
-	"clouditor.io/clouditor/api/auth"
 	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/clientcredentials"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/credentials/insecure"
@@ -44,11 +42,6 @@ import (
 type Authorizer interface {
 	credentials.PerRPCCredentials
 	oauth2.TokenSource
-
-	// AuthURL contains the base URL of the authentication server this authorizer uses. This
-	// can be a gRPC address (for example if the Clouditor internal authentication service is used) or
-	// a regular HTTP(S) URL.
-	AuthURL() string
 }
 
 // UsesAuthorizer is an interface to denote that a struct is willing to accept and use
@@ -58,167 +51,43 @@ type UsesAuthorizer interface {
 	Authorizer() Authorizer
 }
 
-// DefaultInternalAuthorizerAddress specifies the default gRPC address of the internal Clouditor auth service.
-const DefaultInternalAuthorizerAddress = "localhost:9090"
-
-// internalAuthorizer is an authorizer that uses the Clouditor internal auth server (using gRPC) and
-// does a login flow using username and password
-type internalAuthorizer struct {
-	authURL string
-
-	// grpcOptions contains additional grpc dial options
-	grpcOptions []grpc.DialOption
-
-	username string
-	password string
-
-	client auth.AuthenticationClient
-	conn   grpc.ClientConnInterface
-
-	// token contains the current token. It will be checked for expiry in the Token() method.
-	token *oauth2.Token
-
-	// tokenMutex is a Mutex that allowes concurrent access to our token
-	tokenMutex sync.RWMutex
+// oauthAuthorizer is an authorizer that uses OAuth 2.0 client credentials and does a OAuth client
+// credentials flow
+type oauthAuthorizer struct {
+	oauth2.TokenSource
 }
 
-// NewInternalAuthorizerFromPassword creates a new authorizer based on a (gRPC) URL of the
-// authentication server and a username / password combination
-func NewInternalAuthorizerFromPassword(url string, username string, password string, grpcOptions ...grpc.DialOption) Authorizer {
-	return &internalAuthorizer{
-		authURL:     url,
-		username:    username,
-		password:    password,
-		grpcOptions: grpcOptions,
+// NewOAuthAuthorizerFromClientCredentials creates a new authorizer based on an OAuth 2.0 client credentials.
+func NewOAuthAuthorizerFromClientCredentials(config *clientcredentials.Config) Authorizer {
+	var authorizer = &oauthAuthorizer{
+		TokenSource: oauth2.ReuseTokenSource(nil, config.TokenSource(context.Background())),
 	}
-}
 
-// NewInternalAuthorizerFromToken creates a new authorizer based on a (gRPC) URL of the
-// authentication server and an oauth2.Token. It will attempt to refresh an expired access token,
-// if a refresh token is supplied. Note, that this does a similar flow as OAuth 2.0, but it uses
-// our internal direct gRPC connection to the authentication server, whereas OAuth 2.0 would use
-// a POST request with application/x-www-form-urlencoded data.
-func NewInternalAuthorizerFromToken(url string, token *oauth2.Token, grpcOptions ...grpc.DialOption) Authorizer {
-	return &internalAuthorizer{
-		authURL:     url,
-		token:       token,
-		grpcOptions: grpcOptions,
-	}
+	return authorizer
 }
 
-// init initializes the authorizer. This is called when fetching a token, if this authorizer has not been initialized.
-func (i *internalAuthorizer) init() (err error) {
-	// Note, that we do NOT want any credentials.PerRPCCredentials dial option on this connection because
-	// the API of the auth service is available without token authentication. Otherwise, a first login
-	// would be impossible.
-	//
-	// TODO(oxisto): set transport credentials depending on target url, insecure only for localhost
-	opts := []grpc.DialOption{
-		grpc.WithTransportCredentials(insecure.NewCredentials()),
-	}
-
-	// Apply any extra gRPC options that we might have configured. The main use case for this is a unit test,
-	// but clients might want to use this to tweak some values as well.
-	opts = append(opts, i.grpcOptions...)
-
-	if i.conn, err = grpc.Dial(i.authURL, opts...); err != nil {
-		return fmt.Errorf("could not connect: %w", err)
+// NewOAuthAuthorizerFromConfig creates a new authorizer based on an OAuth 2.0 config.
+func NewOAuthAuthorizerFromConfig(config *oauth2.Config, token *oauth2.Token) Authorizer {
+	var authorizer = &oauthAuthorizer{
+		TokenSource: config.TokenSource(context.Background(), token),
 	}
 
-	// Store the client
-	i.client = auth.NewAuthenticationClient(i.conn)
-
-	return nil
-}
-
-// Token is an implementation for the interface oauth2.TokenSource so we can use this authorizer
-// in (almost) the same way as any other OAuth 2.0 token endpoint. It will fetch an access token
-// (and possibly a refresh token), if no token is stored yet in the authorizer or if the token is expired.
-//
-// The "refresh" of a token will either be done by a refresh token (if it exists) or by the stored combination
-// of username / password.
-func (i *internalAuthorizer) Token() (*oauth2.Token, error) {
-	var (
-		resp *auth.TokenResponse
-		err  error
-	)
-
-	// We do a lazy initialization here, so the first request might take a little bit longer.
-	// This might not be entirely thread-safe.
-	if i.conn == nil {
-		err = i.init()
-		if err != nil {
-			return nil, fmt.Errorf("could not initialize connection to auth service: %w", err)
-		}
-	}
-
-	// Lock the token for reading, so that we are sure to get the recent token,
-	// if another call is currently fetching a new one and thus modifying it.
-	i.tokenMutex.RLock()
-
-	// Check if we already have a token and if it is still ok
-	if i.token != nil && i.token.Expiry.After(time.Now()) {
-		// Defer the unlock, if we return here
-		defer i.tokenMutex.RUnlock()
-		return i.token, nil
-	}
-
-	// Unlock a previous read, if we did not return. Otherwise we will block ourselves.
-	i.tokenMutex.RUnlock()
-
-	// Lock the token for modification
-	i.tokenMutex.Lock()
-	// Defer the Unlock so we definitely unlock once we exit this function
-	defer i.tokenMutex.Unlock()
-
-	// Otherwise, we need to re-authenticate
-	// TODO(oxisto): In the future we should split this into two authorizers
-	if i.token != nil && i.token.RefreshToken != "" {
-		resp, err = i.client.Token(context.TODO(), &auth.TokenRequest{
-			GrantType:    "refresh_token",
-			RefreshToken: i.token.RefreshToken,
-		})
-	} else {
-		resp, err = i.client.Login(context.TODO(), &auth.LoginRequest{
-			Username: i.username,
-			Password: i.password,
-		})
-	}
-	if err != nil {
-		// Return without refreshing the token. At this point, the token will still be invalid
-		// and the next call to Token() will try again. This way we can mitigate temporary errors.
-		return nil, fmt.Errorf("error while logging in: %w", err)
-	}
-
-	// Store the current token, if login was successful
-	i.token = &oauth2.Token{
-		AccessToken:  resp.AccessToken,
-		Expiry:       resp.Expiry.AsTime(),
-		TokenType:    resp.TokenType,
-		RefreshToken: resp.RefreshToken,
-	}
-
-	return i.token, nil
-}
-
-// AuthURL is an implementation needed for Authorizer.
-func (i *internalAuthorizer) AuthURL() string {
-	return i.authURL
+	return authorizer
 }
 
 // GetRequestMetadata is an implementation for credentials.PerRPCCredentials. It is called before
 // each RPC request and is used to inject our client credentials into the context of the RPC call.
-func (i *internalAuthorizer) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
+func (p *oauthAuthorizer) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
 	// Fetch a token from our token source. This will also refresh an access token, if it has expired
-	token, err := i.Token()
+	token, err := p.Token()
 	if err != nil {
 		return nil, err
 	}
 
-	if i.RequireTransportSecurity() {
+	if p.RequireTransportSecurity() {
 		ri, _ := credentials.RequestInfoFromContext(ctx)
 		if err = credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
-			return nil, fmt.Errorf("unable to transfer InternalAuthorizer PerRPCCredentials: %v", err)
+			return nil, fmt.Errorf("unable to transfer OAuthAuthorizer PerRPCCredentials: %v", err)
 		}
 	}
 
@@ -227,7 +96,7 @@ func (i *internalAuthorizer) GetRequestMetadata(ctx context.Context, _ ...string
 	}, nil
 }
 
-func (*internalAuthorizer) RequireTransportSecurity() bool {
+func (*oauthAuthorizer) RequireTransportSecurity() bool {
 	// TODO(oxisto): This should be set to true because we transmit credentials (except localhost)
 	return false
 }
diff --git a/api/discovery/discovery.pb.gw.go b/api/discovery/discovery.pb.gw.go
index 6ee987a4..d18be5a3 100644
--- a/api/discovery/discovery.pb.gw.go
+++ b/api/discovery/discovery.pb.gw.go
@@ -111,7 +111,7 @@ func RegisterDiscoveryHandlerServer(ctx context.Context, mux *runtime.ServeMux,
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Discovery/Start")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Discovery/Start", runtime.WithHTTPPathPattern("/v1/discovery/start"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -134,7 +134,7 @@ func RegisterDiscoveryHandlerServer(ctx context.Context, mux *runtime.ServeMux,
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Discovery/Query")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Discovery/Query", runtime.WithHTTPPathPattern("/v1/discovery/query"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -181,7 +181,7 @@ func RegisterDiscoveryHandlerFromEndpoint(ctx context.Context, mux *runtime.Serv
 
 // RegisterDiscoveryHandler registers the http handlers for service Discovery to "mux".
 // The handlers forward requests to the grpc endpoint over "conn".
-func RegisterDiscoveryHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
+func RegisterDiscoveryHandler(ctx context.Context, mux *runtime.ServeMux, conn grpc.ClientConnInterface) error {
 	return RegisterDiscoveryHandlerClient(ctx, mux, NewDiscoveryClient(conn))
 }
 
@@ -196,7 +196,7 @@ func RegisterDiscoveryHandlerClient(ctx context.Context, mux *runtime.ServeMux,
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Discovery/Start")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Discovery/Start", runtime.WithHTTPPathPattern("/v1/discovery/start"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -216,7 +216,7 @@ func RegisterDiscoveryHandlerClient(ctx context.Context, mux *runtime.ServeMux,
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Discovery/Query")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Discovery/Query", runtime.WithHTTPPathPattern("/v1/discovery/query"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
diff --git a/api/evidence/evidence_store.pb.gw.go b/api/evidence/evidence_store.pb.gw.go
index 05ba5129..9c9dc66a 100644
--- a/api/evidence/evidence_store.pb.gw.go
+++ b/api/evidence/evidence_store.pb.gw.go
@@ -95,7 +95,7 @@ func RegisterEvidenceStoreHandlerServer(ctx context.Context, mux *runtime.ServeM
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.EvidenceStore/StoreEvidence")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.EvidenceStore/StoreEvidence", runtime.WithHTTPPathPattern("/v1/evidence_store/evidence"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -118,7 +118,7 @@ func RegisterEvidenceStoreHandlerServer(ctx context.Context, mux *runtime.ServeM
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.EvidenceStore/ListEvidences")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.EvidenceStore/ListEvidences", runtime.WithHTTPPathPattern("/v1/evidence_store/evidences"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -165,7 +165,7 @@ func RegisterEvidenceStoreHandlerFromEndpoint(ctx context.Context, mux *runtime.
 
 // RegisterEvidenceStoreHandler registers the http handlers for service EvidenceStore to "mux".
 // The handlers forward requests to the grpc endpoint over "conn".
-func RegisterEvidenceStoreHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
+func RegisterEvidenceStoreHandler(ctx context.Context, mux *runtime.ServeMux, conn grpc.ClientConnInterface) error {
 	return RegisterEvidenceStoreHandlerClient(ctx, mux, NewEvidenceStoreClient(conn))
 }
 
@@ -180,7 +180,7 @@ func RegisterEvidenceStoreHandlerClient(ctx context.Context, mux *runtime.ServeM
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.EvidenceStore/StoreEvidence")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.EvidenceStore/StoreEvidence", runtime.WithHTTPPathPattern("/v1/evidence_store/evidence"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -200,7 +200,7 @@ func RegisterEvidenceStoreHandlerClient(ctx context.Context, mux *runtime.ServeM
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.EvidenceStore/ListEvidences")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.EvidenceStore/ListEvidences", runtime.WithHTTPPathPattern("/v1/evidence_store/evidences"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
diff --git a/api/orchestrator/orchestrator.pb.gw.go b/api/orchestrator/orchestrator.pb.gw.go
index 28dec76e..4601c761 100644
--- a/api/orchestrator/orchestrator.pb.gw.go
+++ b/api/orchestrator/orchestrator.pb.gw.go
@@ -1118,7 +1118,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/RegisterAssessmentTool")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/RegisterAssessmentTool", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_tools"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1141,7 +1141,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/ListAssessmentTools")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/ListAssessmentTools", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_tools"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1164,7 +1164,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/ListAssessmentTools")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/ListAssessmentTools", runtime.WithHTTPPathPattern("/v1/orchestrator/{metric_id}/assessment_tools"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1187,7 +1187,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/GetAssessmentTool")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/GetAssessmentTool", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_tools/{tool_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1210,7 +1210,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateAssessmentTool")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateAssessmentTool", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_tools/{tool_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1233,7 +1233,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/DeregisterAssessmentTool")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/DeregisterAssessmentTool", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_tools/{tool_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1256,7 +1256,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/StoreAssessmentResult")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/StoreAssessmentResult", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_results"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1279,7 +1279,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/ListAssessmentResults")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/ListAssessmentResults", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_results"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1302,7 +1302,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/CreateMetric")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/CreateMetric", runtime.WithHTTPPathPattern("/v1/orchestrator/metrics"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1325,7 +1325,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateMetric")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateMetric", runtime.WithHTTPPathPattern("/v1/orchestrator/metrics/{metric_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1348,7 +1348,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/GetMetric")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/GetMetric", runtime.WithHTTPPathPattern("/v1/orchestrator/metrics/{metric_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1371,7 +1371,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/ListMetrics")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/ListMetrics", runtime.WithHTTPPathPattern("/v1/orchestrator/metrics"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1394,7 +1394,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/RegisterCloudService")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/RegisterCloudService", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1417,7 +1417,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateCloudService")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateCloudService", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services/{service_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1440,7 +1440,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/GetCloudService")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/GetCloudService", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services/{service_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1463,7 +1463,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/ListCloudServices")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/ListCloudServices", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1486,7 +1486,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/RemoveCloudService")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/RemoveCloudService", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services/{service_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1509,7 +1509,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateMetricConfiguration")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateMetricConfiguration", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services/{service_id}/metric_configurations/{metric_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1532,7 +1532,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/GetMetricConfiguration")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/GetMetricConfiguration", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services/{service_id}/metric_configurations/{metric_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1555,7 +1555,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/ListMetricConfigurations")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/ListMetricConfigurations", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services/{service_id}/metric_configurations"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1578,7 +1578,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateMetricImplementation")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateMetricImplementation", runtime.WithHTTPPathPattern("/v1/orchestrator/metrics/{metric_id}/implementation"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1601,7 +1601,7 @@ func RegisterOrchestratorHandlerServer(ctx context.Context, mux *runtime.ServeMu
 		var stream runtime.ServerTransportStream
 		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/GetMetricImplementation")
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/clouditor.Orchestrator/GetMetricImplementation", runtime.WithHTTPPathPattern("/v1/orchestrator/metrics/{metric_id}/implementation"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1648,7 +1648,7 @@ func RegisterOrchestratorHandlerFromEndpoint(ctx context.Context, mux *runtime.S
 
 // RegisterOrchestratorHandler registers the http handlers for service Orchestrator to "mux".
 // The handlers forward requests to the grpc endpoint over "conn".
-func RegisterOrchestratorHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
+func RegisterOrchestratorHandler(ctx context.Context, mux *runtime.ServeMux, conn grpc.ClientConnInterface) error {
 	return RegisterOrchestratorHandlerClient(ctx, mux, NewOrchestratorClient(conn))
 }
 
@@ -1663,7 +1663,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/RegisterAssessmentTool")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/RegisterAssessmentTool", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_tools"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1683,7 +1683,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/ListAssessmentTools")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/ListAssessmentTools", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_tools"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1703,7 +1703,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/ListAssessmentTools")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/ListAssessmentTools", runtime.WithHTTPPathPattern("/v1/orchestrator/{metric_id}/assessment_tools"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1723,7 +1723,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/GetAssessmentTool")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/GetAssessmentTool", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_tools/{tool_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1743,7 +1743,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateAssessmentTool")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateAssessmentTool", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_tools/{tool_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1763,7 +1763,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/DeregisterAssessmentTool")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/DeregisterAssessmentTool", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_tools/{tool_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1783,7 +1783,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/StoreAssessmentResult")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/StoreAssessmentResult", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_results"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1803,7 +1803,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/ListAssessmentResults")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/ListAssessmentResults", runtime.WithHTTPPathPattern("/v1/orchestrator/assessment_results"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1823,7 +1823,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/CreateMetric")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/CreateMetric", runtime.WithHTTPPathPattern("/v1/orchestrator/metrics"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1843,7 +1843,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateMetric")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateMetric", runtime.WithHTTPPathPattern("/v1/orchestrator/metrics/{metric_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1863,7 +1863,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/GetMetric")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/GetMetric", runtime.WithHTTPPathPattern("/v1/orchestrator/metrics/{metric_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1883,7 +1883,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/ListMetrics")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/ListMetrics", runtime.WithHTTPPathPattern("/v1/orchestrator/metrics"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1903,7 +1903,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/RegisterCloudService")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/RegisterCloudService", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1923,7 +1923,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateCloudService")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateCloudService", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services/{service_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1943,7 +1943,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/GetCloudService")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/GetCloudService", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services/{service_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1963,7 +1963,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/ListCloudServices")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/ListCloudServices", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -1983,7 +1983,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/RemoveCloudService")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/RemoveCloudService", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services/{service_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -2003,7 +2003,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateMetricConfiguration")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateMetricConfiguration", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services/{service_id}/metric_configurations/{metric_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -2023,7 +2023,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/GetMetricConfiguration")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/GetMetricConfiguration", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services/{service_id}/metric_configurations/{metric_id}"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -2043,7 +2043,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/ListMetricConfigurations")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/ListMetricConfigurations", runtime.WithHTTPPathPattern("/v1/orchestrator/cloud_services/{service_id}/metric_configurations"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -2063,7 +2063,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateMetricImplementation")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/UpdateMetricImplementation", runtime.WithHTTPPathPattern("/v1/orchestrator/metrics/{metric_id}/implementation"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
@@ -2083,7 +2083,7 @@ func RegisterOrchestratorHandlerClient(ctx context.Context, mux *runtime.ServeMu
 		ctx, cancel := context.WithCancel(req.Context())
 		defer cancel()
 		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
-		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/GetMetricImplementation")
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/clouditor.Orchestrator/GetMetricImplementation", runtime.WithHTTPPathPattern("/v1/orchestrator/metrics/{metric_id}/implementation"))
 		if err != nil {
 			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
 			return
diff --git a/api/orchestrator/orchestrator.proto b/api/orchestrator/orchestrator.proto
index 949fc7fc..af35b936 100644
--- a/api/orchestrator/orchestrator.proto
+++ b/api/orchestrator/orchestrator.proto
@@ -92,7 +92,8 @@ service Orchestrator {
     };
   }
 
-  // Stores stream of assessment results provided by an assessment tool and returns a response stream. Part of the public API, not exposed as REST.
+  // Stores stream of assessment results provided by an assessment tool and
+  // returns a response stream. Part of the public API, not exposed as REST.
   rpc StoreAssessmentResults(stream StoreAssessmentResultRequest)
       returns (stream StoreAssessmentResultResponse);
 
@@ -181,7 +182,7 @@ service Orchestrator {
       put : "/v1/orchestrator/cloud_services/{service_id}/"
             "metric_configurations/"
             "{metric_id}"
-      body: "configuration"
+      body : "configuration"
     };
   }
 
@@ -215,7 +216,8 @@ service Orchestrator {
   }
 
   // Returns the metric implementation of the passed metric id
-  rpc GetMetricImplementation(GetMetricImplementationRequest) returns (MetricImplementation) {
+  rpc GetMetricImplementation(GetMetricImplementationRequest)
+      returns (MetricImplementation) {
     option (google.api.http) = {
       get : "/v1/orchestrator/metrics/{metric_id}/implementation"
     };
@@ -297,9 +299,7 @@ message UpdateMetricImplementationRequest {
   MetricImplementation implementation = 2;
 }
 
-message GetMetricImplementationRequest {
-  string metric_id = 1;
-}
+message GetMetricImplementationRequest { string metric_id = 1; }
 
 // Represents an external tool or service that offers assessments according to
 // certain metrics
diff --git a/api/orchestrator/orchestrator_grpc.pb.go b/api/orchestrator/orchestrator_grpc.pb.go
index d82f655a..69841037 100644
--- a/api/orchestrator/orchestrator_grpc.pb.go
+++ b/api/orchestrator/orchestrator_grpc.pb.go
@@ -38,7 +38,8 @@ type OrchestratorClient interface {
 	DeregisterAssessmentTool(ctx context.Context, in *DeregisterAssessmentToolRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
 	// Stores the assessment result provided by an assessment tool
 	StoreAssessmentResult(ctx context.Context, in *StoreAssessmentResultRequest, opts ...grpc.CallOption) (*StoreAssessmentResultResponse, error)
-	// Stores stream of assessment results provided by an assessment tool and returns a response stream. Part of the public API, not exposed as REST.
+	// Stores stream of assessment results provided by an assessment tool and
+	// returns a response stream. Part of the public API, not exposed as REST.
 	StoreAssessmentResults(ctx context.Context, opts ...grpc.CallOption) (Orchestrator_StoreAssessmentResultsClient, error)
 	// List all assessment results. Part of the public API, also exposed as REST.
 	ListAssessmentResults(ctx context.Context, in *assessment.ListAssessmentResultsRequest, opts ...grpc.CallOption) (*assessment.ListAssessmentResultsResponse, error)
@@ -321,7 +322,8 @@ type OrchestratorServer interface {
 	DeregisterAssessmentTool(context.Context, *DeregisterAssessmentToolRequest) (*emptypb.Empty, error)
 	// Stores the assessment result provided by an assessment tool
 	StoreAssessmentResult(context.Context, *StoreAssessmentResultRequest) (*StoreAssessmentResultResponse, error)
-	// Stores stream of assessment results provided by an assessment tool and returns a response stream. Part of the public API, not exposed as REST.
+	// Stores stream of assessment results provided by an assessment tool and
+	// returns a response stream. Part of the public API, not exposed as REST.
 	StoreAssessmentResults(Orchestrator_StoreAssessmentResultsServer) error
 	// List all assessment results. Part of the public API, also exposed as REST.
 	ListAssessmentResults(context.Context, *assessment.ListAssessmentResultsRequest) (*assessment.ListAssessmentResultsResponse, error)
diff --git a/cli/commands/commands.go b/cli/commands/commands.go
index 477251d8..f7405899 100644
--- a/cli/commands/commands.go
+++ b/cli/commands/commands.go
@@ -56,6 +56,6 @@ func AddCommands(cmd *cobra.Command) {
 		service.NewServiceCommand(),
 	)
 
-	cmd.PersistentFlags().StringP("session-directory", "s", cli.DefaultSessionFolder, "the directory where the session will be saved and loaded from")
-	_ = viper.BindPFlag("session-directory", cmd.PersistentFlags().Lookup("session-directory"))
+	cmd.PersistentFlags().StringP(cli.SessionFolderFlag, "s", cli.DefaultSessionFolder, "the directory where the session will be saved and loaded from")
+	_ = viper.BindPFlag(cli.SessionFolderFlag, cmd.PersistentFlags().Lookup(cli.SessionFolderFlag))
 }
diff --git a/cli/commands/evidence/evidence.go b/cli/commands/evidence/evidence.go
index 92e72f14..2ee8a97f 100644
--- a/cli/commands/evidence/evidence.go
+++ b/cli/commands/evidence/evidence.go
@@ -26,10 +26,11 @@
 package evidence
 
 import (
-	"clouditor.io/clouditor/api/evidence"
-	"clouditor.io/clouditor/cli"
 	"context"
 	"fmt"
+
+	"clouditor.io/clouditor/api/evidence"
+	"clouditor.io/clouditor/cli"
 	"github.com/spf13/cobra"
 )
 
diff --git a/cli/commands/login/login.go b/cli/commands/login/login.go
index 12f824f7..edd83412 100644
--- a/cli/commands/login/login.go
+++ b/cli/commands/login/login.go
@@ -28,18 +28,41 @@ package login
 import (
 	"context"
 	"fmt"
+	"net"
+	"net/http"
 
-	"clouditor.io/clouditor/api"
-	"clouditor.io/clouditor/api/auth"
 	"clouditor.io/clouditor/cli"
+	oauth2 "github.com/oxisto/oauth2go"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
-	"golang.org/x/oauth2"
 )
 
 const (
-	// URLFlag is the viper flag for the server url
-	URLFlag = "url"
+	// OAuth2ServerFlag is the viper flag for the OAuth 2.0 authorization server.
+	OAuth2ServerFlag = "oauth2-server"
+
+	// OAuth2ClientIDFlag is the viper flag for the OAuth 2.0 client ID.
+	OAuth2ClientIDFlag = "oauth2-client-id"
+
+	// DefaultOAuth2Server is the default OAuth 2.0 authorization server.
+	DefaultOAuth2Server = "http://localhost:8080"
+
+	// DefaultClientID is the default OAuth 2.0 client ID for the CLI.
+	DefaultClientID = "cli"
+
+	// DefaultCallbackServerAddress is the default address for the callback server.
+	DefaultCallbackServerAddress = "localhost:10000"
+)
+
+var (
+	// DefaultCallback is the default callback URL of the callback server.
+	DefaultCallback = fmt.Sprintf("http://%s/callback", DefaultCallbackServerAddress)
+
+	// VerifierGenerator is a function that generates a new verifier.
+	VerifierGenerator = oauth2.GenerateSecret
+
+	// callbackServerReady is an internally used channel to indicate that the callback server is ready.
+	callbackServerReady = make(chan bool)
 )
 
 // NewLoginCommand returns a cobra command for `login` subcommands
@@ -51,42 +74,58 @@ func NewLoginCommand() *cobra.Command {
 		RunE: func(cmd *cobra.Command, args []string) error {
 			var (
 				err     error
-				client  auth.AuthenticationClient
-				res     *auth.TokenResponse
-				req     *auth.LoginRequest
 				session *cli.Session
+				sock    net.Listener
+				code    string
+				config  *oauth2.Config
+				authURL string
 			)
 
-			if session, err = cli.NewSession(args[0]); err != nil {
-				return fmt.Errorf("could not connect: %w", err)
+			// Retrieve the URL of our authentication server
+			authURL = viper.GetString(OAuth2ServerFlag)
+
+			// Create an OAuth 2 config
+			config = &oauth2.Config{
+				ClientID: DefaultClientID,
+				Endpoint: oauth2.Endpoint{
+					AuthURL:  fmt.Sprintf("%s/authorize", authURL),
+					TokenURL: fmt.Sprintf("%s/token", authURL),
+				},
+				RedirectURL: DefaultCallback,
 			}
 
-			if viper.GetString("username") == "" || viper.GetString("password") == "" {
-				if req, err = cli.PromptForLogin(); err != nil {
-					return fmt.Errorf("could not prompt for password: %w", err)
+			srv := newCallbackServer(config)
+
+			go func() {
+				sock, err = net.Listen("tcp", srv.Addr)
+				if err != nil {
+					fmt.Printf("Could not start web server for OAuth 2.0 authorization code flow: %v", err)
 				}
-			} else {
-				req = &auth.LoginRequest{
-					Username: viper.GetString("username"),
-					Password: viper.GetString("password"),
+				go func() {
+					callbackServerReady <- true
+				}()
+
+				err = srv.Serve(sock)
+				if err != http.ErrServerClosed {
+					fmt.Printf("Could not start web server for OAuth 2.0 authorization code flow: %v", err)
+					return
 				}
-			}
+			}()
+			defer srv.Close()
 
-			client = auth.NewAuthenticationClient(session)
+			// waiting for our code
+			code = <-srv.code
+			token, err := srv.config.Exchange(context.Background(), code,
+				oauth2.SetAuthURLParam("code_verifier", srv.verifier),
+			)
 
-			if res, err = client.Login(context.Background(), req); err != nil {
-				return fmt.Errorf("could not login: %w", err)
+			if err != nil {
+				return err
 			}
 
-			// Update the session
-			session.SetAuthorizer(api.NewInternalAuthorizerFromToken(
-				session.Authorizer().AuthURL(),
-				&oauth2.Token{
-					AccessToken:  res.AccessToken,
-					TokenType:    res.TokenType,
-					RefreshToken: res.RefreshToken,
-					Expiry:       res.Expiry.AsTime(),
-				}))
+			if session, err = cli.NewSession(args[0], config, token); err != nil {
+				return fmt.Errorf("could not connect: %w", err)
+			}
 
 			if err = session.Save(); err != nil {
 				return fmt.Errorf("could not save session: %w", err)
@@ -98,11 +137,56 @@ func NewLoginCommand() *cobra.Command {
 		},
 	}
 
-	cmd.PersistentFlags().StringP("username", "u", "", "the username. if not specified, a prompt will be displayed")
-	_ = viper.BindPFlag("username", cmd.PersistentFlags().Lookup("username"))
+	cmd.PersistentFlags().String(OAuth2ServerFlag, DefaultOAuth2Server, "the URL of the OAuth 2.0 server")
+	_ = viper.BindPFlag(OAuth2ServerFlag, cmd.PersistentFlags().Lookup(OAuth2ServerFlag))
 
-	cmd.PersistentFlags().StringP("password", "p", "", "the password. if not specified, a prompt will be displayed")
-	_ = viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password"))
+	cmd.PersistentFlags().String(OAuth2ClientIDFlag, DefaultClientID, "the OAuth 2.0 client ID")
+	_ = viper.BindPFlag(OAuth2ClientIDFlag, cmd.PersistentFlags().Lookup(OAuth2ClientIDFlag))
 
 	return cmd
 }
+
+type callbackServer struct {
+	http.Server
+
+	verifier string
+	config   *oauth2.Config
+	code     chan string
+}
+
+func newCallbackServer(config *oauth2.Config) *callbackServer {
+	var mux = http.NewServeMux()
+
+	var srv = &callbackServer{
+		Server: http.Server{
+			Handler: mux,
+			Addr:    DefaultCallbackServerAddress,
+		},
+		verifier: VerifierGenerator(),
+		config:   config,
+		code:     make(chan string),
+	}
+
+	mux.HandleFunc("/callback", srv.handleCallback)
+
+	challenge := oauth2.GenerateCodeChallenge(srv.verifier)
+	authURL := srv.config.AuthCodeURL("",
+		oauth2.SetAuthURLParam("code_challenge", challenge),
+		oauth2.SetAuthURLParam("code_challenge_method", "S256"),
+	)
+
+	fmt.Printf("Please open %s in your browser 🚀 to continue\n", authURL)
+
+	return srv
+}
+
+func (srv *callbackServer) handleCallback(w http.ResponseWriter, r *http.Request) {
+	var err error
+
+	_, err = w.Write([]byte("Success. You can close this browser tab now"))
+	if err != nil {
+		w.WriteHeader(500)
+	}
+
+	srv.code <- r.URL.Query().Get("code")
+}
diff --git a/cli/commands/service/discovery/discovery.go b/cli/commands/service/discovery/discovery.go
index 0e60dd3a..f02c809c 100644
--- a/cli/commands/service/discovery/discovery.go
+++ b/cli/commands/service/discovery/discovery.go
@@ -26,10 +26,12 @@
 package discovery
 
 import (
-	"clouditor.io/clouditor/api/discovery"
-	"clouditor.io/clouditor/cli"
 	"context"
 	"fmt"
+
+	"clouditor.io/clouditor/api/discovery"
+	"clouditor.io/clouditor/cli"
+
 	"github.com/spf13/cobra"
 )
 
diff --git a/cli/commands/service/orchestrator/orchestrator.go b/cli/commands/service/orchestrator/orchestrator.go
index 5d407351..0970a00d 100644
--- a/cli/commands/service/orchestrator/orchestrator.go
+++ b/cli/commands/service/orchestrator/orchestrator.go
@@ -26,11 +26,12 @@
 package orchestrator
 
 import (
-	"clouditor.io/clouditor/api/assessment"
-	"clouditor.io/clouditor/api/orchestrator"
 	"context"
 	"fmt"
 
+	"clouditor.io/clouditor/api/assessment"
+	"clouditor.io/clouditor/api/orchestrator"
+
 	"clouditor.io/clouditor/cli"
 	"github.com/spf13/cobra"
 )
diff --git a/cli/session.go b/cli/session.go
index 289d546e..9d718585 100644
--- a/cli/session.go
+++ b/cli/session.go
@@ -26,17 +26,14 @@
 package cli
 
 import (
-	"bufio"
 	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
 	"os"
-	"strings"
 
 	"clouditor.io/clouditor/api"
-	"clouditor.io/clouditor/api/auth"
 	"clouditor.io/clouditor/api/orchestrator"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
@@ -49,15 +46,24 @@ import (
 )
 
 var DefaultSessionFolder string
+
+const SessionFolderFlag = "session-directory"
+
 var Output io.Writer = os.Stdout
 
 type Session struct {
 	*grpc.ClientConn
+	*oauth2.Config
+
 	authorizer api.Authorizer
 
+	// URL is the URL of the gRPC server to connect to
 	URL string `json:"url"`
 
 	Folder string `json:"-"`
+
+	// dirty flags that we need to fetch a new token and save the session again
+	dirty bool
 }
 
 func (s *Session) SetAuthorizer(authorizer api.Authorizer) {
@@ -80,12 +86,12 @@ func init() {
 	DefaultSessionFolder = fmt.Sprintf("%s/.clouditor/", home)
 }
 
-func NewSession(url string) (session *Session, err error) {
+func NewSession(url string, config *oauth2.Config, token *oauth2.Token) (session *Session, err error) {
 	session = &Session{
-		URL:    url,
-		Folder: viper.GetString("session-directory"),
-		// We will supply the token later
-		authorizer: api.NewInternalAuthorizerFromToken(url, nil),
+		URL:        url,
+		Folder:     viper.GetString(SessionFolderFlag),
+		authorizer: api.NewOAuthAuthorizerFromConfig(config, token),
+		Config:     config,
 	}
 
 	var opts = []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
@@ -103,7 +109,7 @@ func ContinueSession() (session *Session, err error) {
 		folder string
 	)
 
-	folder = viper.GetString("session-directory")
+	folder = viper.GetString(SessionFolderFlag)
 
 	// try to read from session.json
 	if file, err = os.OpenFile(fmt.Sprintf("%s/session.json", folder), os.O_RDONLY, 0600); err != nil {
@@ -111,14 +117,19 @@ func ContinueSession() (session *Session, err error) {
 	}
 
 	defer func(file *os.File) {
-		err = file.Close()
+		_ = file.Close()
 	}(file)
 
 	session = new(Session)
 	session.Folder = folder
 
 	if err = json.NewDecoder(file).Decode(&session); err != nil {
-		return
+		return nil, fmt.Errorf("could not parse session file: %w", err)
+	}
+
+	// If we detect that this session is "dirty", try to save it again
+	if session.dirty {
+		_ = session.Save()
 	}
 
 	if session.ClientConn, err = grpc.Dial(session.URL, api.DefaultGrpcDialOptions(session)...); err != nil {
@@ -151,6 +162,8 @@ func (s *Session) Save() (err error) {
 		return fmt.Errorf("could not serialize JSON: %w", err)
 	}
 
+	s.dirty = false
+
 	return nil
 }
 
@@ -163,11 +176,13 @@ func (s *Session) MarshalJSON() ([]byte, error) {
 	token, _ := s.authorizer.Token()
 
 	return json.Marshal(&struct {
-		URL   string        `json:"url"`
-		Token *oauth2.Token `json:"token"`
+		URL    string         `json:"url"`
+		Token  *oauth2.Token  `json:"token"`
+		Config *oauth2.Config `json:"oauth2"`
 	}{
-		URL:   s.URL,
-		Token: token,
+		URL:    s.URL,
+		Token:  token,
+		Config: s.Config,
 	})
 }
 
@@ -176,8 +191,9 @@ func (s *Session) MarshalJSON() ([]byte, error) {
 // necessary.
 func (s *Session) UnmarshalJSON(data []byte) (err error) {
 	v := struct {
-		URL   string        `json:"url"`
-		Token *oauth2.Token `json:"token"`
+		URL    string         `json:"url"`
+		Token  *oauth2.Token  `json:"token"`
+		Config *oauth2.Config `json:"oauth2"`
 	}{}
 
 	if err = json.Unmarshal(data, &v); err != nil {
@@ -185,7 +201,17 @@ func (s *Session) UnmarshalJSON(data []byte) (err error) {
 	}
 
 	s.URL = v.URL
-	s.authorizer = api.NewInternalAuthorizerFromToken(s.URL, v.Token)
+
+	// Mark this session as dirty, if the token is not valid (anymore)
+	s.dirty = !v.Token.Valid()
+	s.Config = v.Config
+
+	// Check, if oauth config is missing or invalid
+	if s.Config == nil {
+		return errors.New("missing oauth2 config")
+	}
+
+	s.authorizer = api.NewOAuthAuthorizerFromConfig(s.Config, v.Token)
 	return
 }
 
@@ -217,32 +243,6 @@ func (*Session) HandleResponse(msg proto.Message, err error) error {
 	return err
 }
 
-func PromptForLogin() (loginRequest *auth.LoginRequest, err error) {
-	reader := bufio.NewReader(os.Stdin)
-
-	fmt.Print("Enter username: ")
-	username, err := reader.ReadString('\n')
-	fmt.Println()
-
-	if err != nil {
-		return
-	}
-
-	fmt.Print("Enter password: ")
-	password, err := reader.ReadString('\n')
-
-	if err != nil {
-		return
-	}
-
-	loginRequest = &auth.LoginRequest{
-		Username: strings.Trim(username, "\n"),
-		Password: strings.Trim(password, "\n"),
-	}
-
-	return loginRequest, nil
-}
-
 func DefaultArgsShellComp(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
 	return []string{}, cobra.ShellCompDirectiveNoFileComp
 }
diff --git a/cmd/engine/engine.go b/cmd/engine/engine.go
index abcb396d..cc1dcb85 100644
--- a/cmd/engine/engine.go
+++ b/cmd/engine/engine.go
@@ -34,20 +34,12 @@ import (
 	"os"
 	"strings"
 
-	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
-	grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
-	grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
-	grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags"
-	"github.com/sirupsen/logrus"
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-
-	"clouditor.io/clouditor/api"
 	"clouditor.io/clouditor/api/assessment"
-	"clouditor.io/clouditor/api/auth"
 	"clouditor.io/clouditor/api/discovery"
 	"clouditor.io/clouditor/api/evidence"
 	"clouditor.io/clouditor/api/orchestrator"
+	commands_login "clouditor.io/clouditor/cli/commands/login"
+	"clouditor.io/clouditor/internal/auth"
 	"clouditor.io/clouditor/logging/formatter"
 	"clouditor.io/clouditor/persistence"
 	"clouditor.io/clouditor/persistence/gorm"
@@ -55,53 +47,73 @@ import (
 	"clouditor.io/clouditor/rest"
 	"clouditor.io/clouditor/service"
 	service_assessment "clouditor.io/clouditor/service/assessment"
-	service_auth "clouditor.io/clouditor/service/auth"
 	service_discovery "clouditor.io/clouditor/service/discovery"
 	service_evidenceStore "clouditor.io/clouditor/service/evidence"
 	service_orchestrator "clouditor.io/clouditor/service/orchestrator"
 
+	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
+	grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
+	grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
+	grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags"
+	oauth2 "github.com/oxisto/oauth2go"
+	"github.com/oxisto/oauth2go/login"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+	"golang.org/x/oauth2/clientcredentials"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/reflection"
 )
 
 const (
-	APIDefaultUserFlag         = "api-default-user"
-	APIDefaultPasswordFlag     = "api-default-password"
-	APIKeyPasswordFlag         = "api-key-password"
-	APIKeyPathFlag             = "api-key-path"
-	APIKeySaveOnCreateFlag     = "api-key-save-on-create"
-	APIgRPCPortFlag            = "api-grpc-port"
-	APIHTTPPortFlag            = "api-http-port"
-	APICORSAllowedOriginsFlags = "api-cors-allowed-origins"
-	APICORSAllowedHeadersFlags = "api-cors-allowed-headers"
-	APICORSAllowedMethodsFlags = "api-cors-allowed-methods"
-	APIJWKSURLFlag             = "api-jwks-url"
-	DBUserNameFlag             = "db-user-name"
-	DBPasswordFlag             = "db-password"
-	DBHostFlag                 = "db-host"
-	DBNameFlag                 = "db-name"
-	DBPortFlag                 = "db-port"
-	DBInMemoryFlag             = "db-in-memory"
-	CreateDefaultTarget        = "target-default-create"
-	ProviderFlag               = "provider"
-
-	DefaultAPIDefaultUser      = "clouditor"
-	DefaultAPIDefaultPassword  = "clouditor"
-	DefaultAPIgRPCPort         = 9090
-	DefaultDBUserName          = "postgres"
-	DefaultDBPassword          = "postgres"
-	DefaultDBHost              = "localhost"
-	DefaultDBName              = "postgres"
-	DefaultDBPort              = 5432
-	DefaultDBInMemory          = false
-	DefaultCreateDefaultTarget = true
+	APIDefaultUserFlag               = "api-default-user"
+	APIDefaultPasswordFlag           = "api-default-password"
+	APIKeyPasswordFlag               = "api-key-password"
+	APIKeyPathFlag                   = "api-key-path"
+	APIKeySaveOnCreateFlag           = "api-key-save-on-create"
+	APIgRPCPortFlag                  = "api-grpc-port"
+	APIHTTPPortFlag                  = "api-http-port"
+	APICORSAllowedOriginsFlags       = "api-cors-allowed-origins"
+	APICORSAllowedHeadersFlags       = "api-cors-allowed-headers"
+	APICORSAllowedMethodsFlags       = "api-cors-allowed-methods"
+	APIJWKSURLFlag                   = "api-jwks-url"
+	APIStartEmbeddedOAuth2ServerFlag = "api-start-embedded-oauth-server"
+	ServiceOAuth2EndpointFlag        = "service-oauth2-token-endpoint"
+	ServiceOAuth2ClientIDFlag        = "service-oauth2-client-id"
+	ServiceOAuth2ClientSecretFlag    = "service-oauth2-client-secret"
+	DBUserNameFlag                   = "db-user-name"
+	DBPasswordFlag                   = "db-password"
+	DBHostFlag                       = "db-host"
+	DBNameFlag                       = "db-name"
+	DBPortFlag                       = "db-port"
+	DBInMemoryFlag                   = "db-in-memory"
+	CreateDefaultTarget              = "target-default-create"
+	DiscoveryAutoStartFlag           = "discovery-auto-start"
+	DiscoveryProviderFlag            = "discovery-provider"
+	DashboardURLFlag                 = "dashboard-url"
+
+	DefaultAPIDefaultUser               = "clouditor"
+	DefaultAPIDefaultPassword           = "clouditor"
+	DefaultAPIgRPCPort                  = 9090
+	DefaultAPIStartEmbeddedOAuth2Server = true
+	DefaultServiceOAuth2Endpoint        = "http://localhost:8080/token"
+	DefaultServiceOAuth2ClientID        = "clouditor"
+	DefaultServiceOAuth2ClientSecret    = "clouditor"
+	DefaultDBUserName                   = "postgres"
+	DefaultDBPassword                   = "postgres"
+	DefaultDBHost                       = "localhost"
+	DefaultDBName                       = "postgres"
+	DefaultDBPort                       = 5432
+	DefaultDBInMemory                   = false
+	DefaultCreateDefaultTarget          = true
+	DefaultDiscoveryAutoStart           = false
+	DefaultDashboardURL                 = "http://localhost:8080"
 
 	EnvPrefix = "CLOUDITOR"
 )
 
 var (
 	server               *grpc.Server
-	authService          *service_auth.Service
 	discoveryService     discovery.DiscoveryServer
 	orchestratorService  *service_orchestrator.Service
 	assessmentService    assessment.AssessmentServer
@@ -127,12 +139,16 @@ func init() {
 
 	engineCmd.Flags().String(APIDefaultUserFlag, DefaultAPIDefaultUser, "Specifies the default API username")
 	engineCmd.Flags().String(APIDefaultPasswordFlag, DefaultAPIDefaultPassword, "Specifies the default API password")
-	engineCmd.Flags().String(APIKeyPasswordFlag, service_auth.DefaultApiKeyPassword, "Specifies the password used to proctect the API private key")
-	engineCmd.Flags().String(APIKeyPathFlag, service_auth.DefaultApiKeyPath, "Specifies the location of the API private key")
-	engineCmd.Flags().Bool(APIKeySaveOnCreateFlag, service_auth.DefaultApiKeySaveOnCreate, "Specifies whether the API key should be saved on creation. It will only created if the default location is used.")
+	engineCmd.Flags().String(APIKeyPasswordFlag, auth.DefaultApiKeyPassword, "Specifies the password used to proctect the API private key")
+	engineCmd.Flags().String(APIKeyPathFlag, auth.DefaultApiKeyPath, "Specifies the location of the API private key")
+	engineCmd.Flags().Bool(APIKeySaveOnCreateFlag, auth.DefaultApiKeySaveOnCreate, "Specifies whether the API key should be saved on creation. It will only created if the default location is used.")
 	engineCmd.Flags().Int16(APIgRPCPortFlag, DefaultAPIgRPCPort, "Specifies the port used for the gRPC API")
 	engineCmd.Flags().Int16(APIHTTPPortFlag, rest.DefaultAPIHTTPPort, "Specifies the port used for the HTTP API")
 	engineCmd.Flags().String(APIJWKSURLFlag, service.DefaultJWKSURL, "Specifies the JWKS URL used to verify authentication tokens in the gRPC and HTTP API")
+	engineCmd.Flags().String(ServiceOAuth2EndpointFlag, DefaultServiceOAuth2Endpoint, "Specifies the OAuth 2.0 token endpoint")
+	engineCmd.Flags().String(ServiceOAuth2ClientIDFlag, DefaultServiceOAuth2ClientID, "Specifies the OAuth 2.0 client ID")
+	engineCmd.Flags().String(ServiceOAuth2ClientSecretFlag, DefaultServiceOAuth2ClientSecret, "Specifies the OAuth 2.0 client secret")
+	engineCmd.Flags().Bool(APIStartEmbeddedOAuth2ServerFlag, DefaultAPIStartEmbeddedOAuth2Server, "Specifies whether the embedded OAuth 2.0 authorization server is started as part of the REST gateway. For production workloads, an external authorization server is recommended.")
 	engineCmd.Flags().StringArray(APICORSAllowedOriginsFlags, rest.DefaultAllowedOrigins, "Specifies the origins allowed in CORS")
 	engineCmd.Flags().StringArray(APICORSAllowedHeadersFlags, rest.DefaultAllowedHeaders, "Specifies the headers allowed in CORS")
 	engineCmd.Flags().StringArray(APICORSAllowedMethodsFlags, rest.DefaultAllowedMethods, "Specifies the methods allowed in CORS")
@@ -143,7 +159,9 @@ func init() {
 	engineCmd.Flags().Int16(DBPortFlag, DefaultDBPort, "Provides port for database")
 	engineCmd.Flags().Bool(DBInMemoryFlag, DefaultDBInMemory, "Uses an in-memory database which is not persisted at all")
 	engineCmd.Flags().Bool(CreateDefaultTarget, DefaultCreateDefaultTarget, "Creates a default target cloud service if it does not exist")
-	engineCmd.Flags().StringSliceVarP(&providers, ProviderFlag, "p", []string{}, "Providers to discover, separated by comma")
+	engineCmd.Flags().Bool(DiscoveryAutoStartFlag, DefaultDiscoveryAutoStart, "Automatically start the discovery when engine starts")
+	engineCmd.Flags().StringSliceP(DiscoveryProviderFlag, "p", []string{}, "Providers to discover, separated by comma")
+	engineCmd.Flags().String(DashboardURLFlag, DefaultDashboardURL, "The URL of the Clouditor Dashboard. If the embedded server is used, a public OAuth 2.0 client based on this URL will be added")
 
 	_ = viper.BindPFlag(APIDefaultUserFlag, engineCmd.Flags().Lookup(APIDefaultUserFlag))
 	_ = viper.BindPFlag(APIDefaultPasswordFlag, engineCmd.Flags().Lookup(APIDefaultPasswordFlag))
@@ -153,6 +171,10 @@ func init() {
 	_ = viper.BindPFlag(APIgRPCPortFlag, engineCmd.Flags().Lookup(APIgRPCPortFlag))
 	_ = viper.BindPFlag(APIHTTPPortFlag, engineCmd.Flags().Lookup(APIHTTPPortFlag))
 	_ = viper.BindPFlag(APIJWKSURLFlag, engineCmd.Flags().Lookup(APIJWKSURLFlag))
+	_ = viper.BindPFlag(ServiceOAuth2EndpointFlag, engineCmd.Flags().Lookup(ServiceOAuth2EndpointFlag))
+	_ = viper.BindPFlag(ServiceOAuth2ClientIDFlag, engineCmd.Flags().Lookup(ServiceOAuth2ClientIDFlag))
+	_ = viper.BindPFlag(ServiceOAuth2ClientSecretFlag, engineCmd.Flags().Lookup(ServiceOAuth2ClientSecretFlag))
+	_ = viper.BindPFlag(APIStartEmbeddedOAuth2ServerFlag, engineCmd.Flags().Lookup(APIStartEmbeddedOAuth2ServerFlag))
 	_ = viper.BindPFlag(APICORSAllowedOriginsFlags, engineCmd.Flags().Lookup(APICORSAllowedOriginsFlags))
 	_ = viper.BindPFlag(APICORSAllowedHeadersFlags, engineCmd.Flags().Lookup(APICORSAllowedHeadersFlags))
 	_ = viper.BindPFlag(APICORSAllowedMethodsFlags, engineCmd.Flags().Lookup(APICORSAllowedMethodsFlags))
@@ -163,7 +185,9 @@ func init() {
 	_ = viper.BindPFlag(DBPortFlag, engineCmd.Flags().Lookup(DBPortFlag))
 	_ = viper.BindPFlag(DBInMemoryFlag, engineCmd.Flags().Lookup(DBInMemoryFlag))
 	_ = viper.BindPFlag(CreateDefaultTarget, engineCmd.Flags().Lookup(CreateDefaultTarget))
-	_ = viper.BindPFlag(ProviderFlag, engineCmd.Flags().Lookup(ProviderFlag))
+	_ = viper.BindPFlag(DiscoveryAutoStartFlag, engineCmd.Flags().Lookup(DiscoveryAutoStartFlag))
+	_ = viper.BindPFlag(DiscoveryProviderFlag, engineCmd.Flags().Lookup(DiscoveryProviderFlag))
+	_ = viper.BindPFlag(DashboardURLFlag, engineCmd.Flags().Lookup(DashboardURLFlag))
 }
 
 func initConfig() {
@@ -201,32 +225,33 @@ func doCmd(_ *cobra.Command, _ []string) (err error) {
 	}
 
 	// If no CSPs for discovering is given, take all implemented discoverers
-	if viper.GetString(ProviderFlag) == "" {
+	if viper.GetString(DiscoveryProviderFlag) == "" {
 		providers = []string{service_discovery.ProviderAWS, service_discovery.ProviderAzure, service_discovery.ProviderK8S}
 	} else {
-		providers = []string{viper.GetString(ProviderFlag)}
+		providers = []string{viper.GetString(DiscoveryProviderFlag)}
 	}
 
-	authService = service_auth.NewService(
-		service_auth.WithStorage(db),
-		service_auth.WithApiKeyPassword(viper.GetString(APIKeyPasswordFlag)),
-		service_auth.WithApiKeyPath(viper.GetString(APIKeyPathFlag)),
-		service_auth.WithApiKeySaveOnCreate(viper.GetBool(APIKeySaveOnCreateFlag)),
-	)
 	discoveryService = service_discovery.NewService(
-		service_discovery.WithInternalAuthorizer(
-			api.DefaultInternalAuthorizerAddress,
-			viper.GetString(APIDefaultUserFlag),
-			viper.GetString(APIDefaultPasswordFlag),
-		),
 		service_discovery.WithProviders(providers),
+		service_discovery.WithOAuth2Authorizer(
+			// Configure the OAuth 2.0 client credentials for this service
+			&clientcredentials.Config{
+				ClientID:     viper.GetString(ServiceOAuth2ClientIDFlag),
+				ClientSecret: viper.GetString(ServiceOAuth2ClientSecretFlag),
+				TokenURL:     viper.GetString(ServiceOAuth2EndpointFlag),
+			}),
 	)
+
 	orchestratorService = service_orchestrator.NewService(service_orchestrator.WithStorage(db))
+
 	assessmentService = service_assessment.NewService(
-		service_assessment.WithInternalAuthorizer(
-			api.DefaultInternalAuthorizerAddress,
-			viper.GetString(APIDefaultUserFlag),
-			viper.GetString(APIDefaultPasswordFlag),
+		service_assessment.WithOAuth2Authorizer(
+			// Configure the OAuth 2.0 client credentials for this service
+			&clientcredentials.Config{
+				ClientID:     viper.GetString(ServiceOAuth2ClientIDFlag),
+				ClientSecret: viper.GetString(ServiceOAuth2ClientSecretFlag),
+				TokenURL:     viper.GetString(ServiceOAuth2EndpointFlag),
+			},
 		),
 	)
 	evidenceStoreService = service_evidenceStore.NewService()
@@ -240,11 +265,6 @@ func doCmd(_ *cobra.Command, _ []string) (err error) {
 	// evidenceStoreService.RegisterEvidenceHook(func(result *evidence.Evidence, err error) {})
 	// assessmentService.RegisterAssessmentResultHook(func(result *assessment.AssessmentResult, err error) {}
 
-	err = authService.CreateDefaultUser(viper.GetString(APIDefaultUserFlag), viper.GetString(APIDefaultPasswordFlag))
-	if err != nil {
-		log.Errorf("Could not create default user: %v", err)
-	}
-
 	if viper.GetBool(CreateDefaultTarget) {
 		_, err := orchestratorService.CreateDefaultTargetCloudService()
 		if err != nil {
@@ -283,7 +303,6 @@ func doCmd(_ *cobra.Command, _ []string) (err error) {
 			grpc_logrus.StreamServerInterceptor(grpcLoggerEntry),
 			grpc_auth.StreamServerInterceptor(authConfig.AuthFunc),
 		))
-	auth.RegisterAuthenticationServer(server, authService)
 	discovery.RegisterDiscoveryServer(server, discoveryService)
 	orchestrator.RegisterOrchestratorServer(server, orchestratorService)
 	assessment.RegisterAssessmentServer(server, assessmentService)
@@ -292,14 +311,58 @@ func doCmd(_ *cobra.Command, _ []string) (err error) {
 	// enable reflection, primary for testing in early stages
 	reflection.Register(server)
 
+	var opts []rest.ServerConfigOption = []rest.ServerConfigOption{
+		rest.WithAllowedOrigins(viper.GetStringSlice(APICORSAllowedOriginsFlags)),
+		rest.WithAllowedHeaders(viper.GetStringSlice(APICORSAllowedHeadersFlags)),
+		rest.WithAllowedMethods(viper.GetStringSlice(APICORSAllowedMethodsFlags)),
+	}
+
+	// Let's check, if we are using our embedded OAuth 2.0 server, which we need to start
+	// (using additional arguments to our existing REST gateway). In a production scenario
+	// the usage of a dedicated (external) OAuth 2.0 server is recommended. In order
+	// to configure the external server, the flags ServiceOAuth2EndpointFlag and APIJWKSURLFlag
+	// can be used.
+	if viper.GetBool(APIStartEmbeddedOAuth2ServerFlag) {
+		opts = []rest.ServerConfigOption{
+			rest.WithEmbeddedOAuth2Server(
+				viper.GetString(APIKeyPathFlag),
+				viper.GetString(APIKeyPasswordFlag),
+				viper.GetBool(APIKeySaveOnCreateFlag),
+				// Create a public client for our CLI
+				oauth2.WithClient(
+					commands_login.DefaultClientID,
+					"",
+					commands_login.DefaultCallback,
+				),
+				// Create a public client for our dashboard
+				oauth2.WithClient(
+					"dashboard",
+					"",
+					fmt.Sprintf("%s/callback", viper.GetString(DashboardURLFlag)),
+				),
+				// Createa a confidential client with default credentials for our services
+				oauth2.WithClient(
+					viper.GetString(ServiceOAuth2ClientIDFlag),
+					viper.GetString(ServiceOAuth2ClientIDFlag),
+					"",
+				),
+				// Createa a default user for logging in
+				login.WithLoginPage(
+					login.WithUser(
+						viper.GetString(APIDefaultUserFlag),
+						viper.GetString(APIDefaultPasswordFlag),
+					),
+				),
+			),
+		}
+	}
+
 	// start the gRPC-HTTP gateway
 	go func() {
 		err = rest.RunServer(context.Background(),
 			grpcPort,
 			httpPort,
-			rest.WithAllowedOrigins(viper.GetStringSlice(APICORSAllowedOriginsFlags)),
-			rest.WithAllowedHeaders(viper.GetStringSlice(APICORSAllowedHeadersFlags)),
-			rest.WithAllowedMethods(viper.GetStringSlice(APICORSAllowedMethodsFlags)),
+			opts...,
 		)
 		if errors.Is(err, http.ErrServerClosed) {
 			// ToDo(oxisto): deepsource anti-pattern: calls to os.Exit only in main() or init() functions
@@ -315,6 +378,17 @@ func doCmd(_ *cobra.Command, _ []string) (err error) {
 
 	log.Infof("Starting gRPC endpoint on :%d", grpcPort)
 
+	// Automatically start the discovery, if we have this flag enabled
+	if viper.GetBool(DiscoveryAutoStartFlag) {
+		go func() {
+			<-rest.GetReadyChannel()
+			_, err = discoveryService.Start(context.Background(), &discovery.StartDiscoveryRequest{})
+			if err != nil {
+				log.Errorf("Could not automatically start discovery: %v", err)
+			}
+		}()
+	}
+
 	// serve the gRPC socket
 	if err := server.Serve(sock); err != nil {
 		log.Infof("Failed to serve gRPC endpoint: %s", err)
diff --git a/go.mod b/go.mod
index 2aa347b9..e58fd003 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,6 @@ require (
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.9
 	github.com/Azure/go-autorest/autorest/to v0.4.0
 	github.com/MicahParks/keyfunc v1.0.1
-	github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8
 	github.com/aws/aws-sdk-go-v2 v1.14.0
 	github.com/aws/aws-sdk-go-v2/config v1.14.0
 	github.com/aws/aws-sdk-go-v2/service/ec2 v1.30.0
@@ -27,11 +26,12 @@ require (
 	github.com/logrusorgru/aurora/v3 v3.0.0
 	github.com/mitchellh/mapstructure v1.4.3
 	github.com/open-policy-agent/opa v0.38.0
+	github.com/oxisto/oauth2go v0.5.9
 	github.com/sirupsen/logrus v1.8.1
 	github.com/spf13/cobra v1.3.0
 	github.com/spf13/viper v1.10.0
 	github.com/stretchr/testify v1.7.0
-	golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
+	golang.org/x/crypto v0.0.0-20220214200702-86341886e292
 	golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
 	google.golang.org/genproto v0.0.0-20220302033224-9aa15565e42a
 	google.golang.org/grpc v1.44.0
@@ -120,7 +120,7 @@ require (
 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b // indirect
-	golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
+	golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
 	golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
 	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
diff --git a/go.sum b/go.sum
index aa26d29b..517273b5 100644
--- a/go.sum
+++ b/go.sum
@@ -93,8 +93,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8 h1:ZtMr6/tt7VU/Ijpyyedn7eUwwsNX1uskEcR+maLEF18=
-github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8/go.mod h1:Kmn5t2Rb93Q4NTprN4+CCgARGvigKMJyxP0WckpTUp0=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@@ -572,6 +570,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
 github.com/open-policy-agent/opa v0.38.0 h1:XuGM6yZmHCPO4o1JJGs8pKuoC/vMFkr4pjBVhFLrOk8=
 github.com/open-policy-agent/opa v0.38.0/go.mod h1:z0+Gw2+Re8cEf4/GjHr/wAL1diGy8BkhICIiCUb8y6A=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/oxisto/oauth2go v0.5.9 h1:UfQyX6D13jG3ExV9Mc3AGnhuTDz+lrgVzOSekm1X27o=
+github.com/oxisto/oauth2go v0.5.9/go.mod h1:fwpFZP2oyIbpuVr3I2fYzYqOMdVK06rdR9rLl4S41Pk=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -738,13 +738,12 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
-golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
+golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -830,8 +829,10 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
diff --git a/internal/auth/key_loader.go b/internal/auth/key_loader.go
new file mode 100644
index 00000000..a2cb680c
--- /dev/null
+++ b/internal/auth/key_loader.go
@@ -0,0 +1,227 @@
+// Copyright 2016-2020 Fraunhofer AISEC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//           $$\                           $$\ $$\   $$\
+//           $$ |                          $$ |\__|  $$ |
+//  $$$$$$$\ $$ | $$$$$$\  $$\   $$\  $$$$$$$ |$$\ $$$$$$\    $$$$$$\   $$$$$$\
+// $$  _____|$$ |$$  __$$\ $$ |  $$ |$$  __$$ |$$ |\_$$  _|  $$  __$$\ $$  __$$\
+// $$ /      $$ |$$ /  $$ |$$ |  $$ |$$ /  $$ |$$ |  $$ |    $$ /  $$ |$$ | \__|
+// $$ |      $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$\ $$ |  $$ |$$ |
+// \$$$$$$\  $$ |\$$$$$   |\$$$$$   |\$$$$$$  |$$ |  \$$$   |\$$$$$   |$$ |
+//  \_______|\__| \______/  \______/  \_______|\__|   \____/  \______/ \__|
+//
+// This file is part of Clouditor Community Edition.
+
+package auth
+
+import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/user"
+	"path/filepath"
+	"strings"
+
+	"github.com/golang-jwt/jwt/v4"
+	"github.com/sirupsen/logrus"
+)
+
+var log *logrus.Entry
+
+func init() {
+	log = logrus.WithField("component", "auth")
+}
+
+const (
+	// DefaultApiKeySaveOnCreate specifies whether a created API key will be saved. This is useful to turn of in unit tests, where
+	// we only want a temporary key.
+	DefaultApiKeySaveOnCreate = true
+
+	// DefaultApiKeyPassword is the default password to protect the API key
+	DefaultApiKeyPassword = "changeme"
+
+	// DefaultApiKeyPath is the default path for the API private key
+	DefaultApiKeyPath = "~/.clouditor/api.key"
+)
+
+// UserClaims extend jwt.StandardClaims with more detailed claims about a user
+type UserClaims struct {
+	jwt.RegisteredClaims
+	FullName string `json:"full_name"`
+	EMail    string `json:"email"`
+}
+
+type keyLoader struct {
+	path         string
+	password     string
+	saveOnCreate bool
+}
+
+// LoadSigningKeys implements a singing keys func for our internal authorization server
+func LoadSigningKeys(path string, password string, saveOnCreate bool) map[int]*ecdsa.PrivateKey {
+	// create a key loader with our arguments
+	loader := keyLoader{
+		path:         path,
+		password:     password,
+		saveOnCreate: saveOnCreate,
+	}
+
+	return map[int]*ecdsa.PrivateKey{
+		0: loader.LoadKey(),
+	}
+}
+
+func (l *keyLoader) LoadKey() (key *ecdsa.PrivateKey) {
+	var (
+		err error
+	)
+
+	// Try to load the key from the given path
+	key, err = loadKeyFromFile(l.path, []byte(l.password))
+	if err != nil {
+		key = l.recoverFromLoadApiKeyError(err, l.path == DefaultApiKeyPath)
+	}
+
+	return
+}
+
+// recoverFromLoadApiKeyError tries to recover from an error during key loading. We treat different errors differently.
+// For example if the path is the default path and the error is os.ErrNotExist, this could be just the first start of Clouditor.
+// So we only treat this as an information that we will create a new key, which we will save, based on the config.
+//
+// If the user specifies a custom path and this one does not exist, we will report an error
+// here.
+func (l *keyLoader) recoverFromLoadApiKeyError(err error, defaultPath bool) (key *ecdsa.PrivateKey) {
+	// In any case, create a new temporary API key
+	key, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+
+	if defaultPath && errors.Is(err, os.ErrNotExist) {
+		log.Infof("API key does not exist at the default location yet. We will create a new one")
+
+		if l.saveOnCreate {
+			// Also save the key in this case, so we can load it next time
+			err = saveKeyToFile(key, l.path, l.password)
+
+			// Error while error handling, meh
+			if err != nil {
+				log.Errorf("Error while saving the new API key: %v", err)
+			}
+		}
+	} else if err != nil {
+		log.Errorf("Could not load key from file, continuing with a temporary key: %v", err)
+	}
+
+	return key
+}
+
+// loadKeyFromFile loads an ecdsa.PrivateKey from a path. The key must in PEM format and protected by
+// a password using PKCS#8 with PBES2.
+func loadKeyFromFile(path string, password []byte) (key *ecdsa.PrivateKey, err error) {
+	var (
+		keyFile string
+	)
+
+	keyFile, err = expandPath(path)
+	if err != nil {
+		return nil, fmt.Errorf("error while expanding path: %w", err)
+	}
+
+	if _, err = os.Stat(keyFile); os.IsNotExist(err) {
+		return nil, fmt.Errorf("file does not exist (yet): %w", err)
+	}
+
+	// Check, if we already have a persisted API key
+	f, err := os.OpenFile(keyFile, os.O_RDONLY, 0600)
+	if err != nil {
+		return nil, fmt.Errorf("error while opening the file: %w", err)
+	}
+	defer func() {
+		err := f.Close()
+		if err != nil {
+			log.Errorf("Error while closing file: %v", err)
+		}
+	}()
+
+	data, err := ioutil.ReadAll(f)
+	if err != nil {
+		return nil, fmt.Errorf("error while reading file content: %w", err)
+	}
+
+	key, err = ParseECPrivateKeyFromPEMWithPassword(data, password)
+	if err != nil {
+		return nil, fmt.Errorf("error while parsing private key: %w", err)
+	}
+
+	return key, nil
+}
+
+// saveKeyToFile saves an ecdsa.PrivateKey to a path. The key will be saved in PEM format and protected by
+// a password using PKCS#8 with PBES2.
+func saveKeyToFile(apiKey *ecdsa.PrivateKey, keyPath string, password string) (err error) {
+	keyPath, err = expandPath(keyPath)
+	if err != nil {
+		return fmt.Errorf("error while expanding path: %w", err)
+	}
+
+	// Check, if we already have a persisted API key
+	f, err := os.OpenFile(keyPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
+	if err != nil {
+		return fmt.Errorf("error while opening the file: %w", err)
+	}
+	defer func() {
+		err := f.Close()
+		if err != nil {
+			log.Errorf("Error while closing file: %v", err)
+		}
+	}()
+
+	data, err := MarshalECPrivateKeyWithPassword(apiKey, []byte(password))
+	if err != nil {
+		return fmt.Errorf("error while marshalling private key: %w", err)
+	}
+
+	_, err = f.Write(data)
+	if err != nil {
+		return fmt.Errorf("error while writing file content: %w", err)
+	}
+
+	return nil
+}
+
+// expandPath expands a path that possible contains a tilde (~) character into the home directory
+// of the user
+func expandPath(path string) (out string, err error) {
+	var (
+		u *user.User
+	)
+
+	// Fetch the current user home directory
+	u, err = user.Current()
+	if err != nil {
+		return path, fmt.Errorf("could not find retrieve current user: %w", err)
+	}
+
+	if path == "~" {
+		return u.HomeDir, nil
+	} else if strings.HasPrefix(path, "~") {
+		// We only allow ~ at the beginning of the path
+		return filepath.Join(u.HomeDir, path[2:]), nil
+	}
+
+	return path, nil
+}
diff --git a/rest/rest.go b/rest/rest.go
index 182d35c7..a54644f6 100644
--- a/rest/rest.go
+++ b/rest/rest.go
@@ -27,6 +27,7 @@ package rest
 
 import (
 	"context"
+	"crypto/ecdsa"
 	"errors"
 	"fmt"
 	"net"
@@ -37,13 +38,13 @@ import (
 	"time"
 
 	"clouditor.io/clouditor/api/assessment"
-	"clouditor.io/clouditor/api/auth"
 	"clouditor.io/clouditor/api/discovery"
 	"clouditor.io/clouditor/api/evidence"
 	"clouditor.io/clouditor/api/orchestrator"
-	service_auth "clouditor.io/clouditor/service/auth"
+	"clouditor.io/clouditor/internal/auth"
 
 	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
+	oauth2 "github.com/oxisto/oauth2go"
 	"github.com/sirupsen/logrus"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials/insecure"
@@ -95,6 +96,17 @@ var (
 	DefaultAPIHTTPPort int16 = 8080
 )
 
+func init() {
+	log = logrus.WithField("component", "rest")
+
+	// initialize the CORS config with restrictive default values, e.g. no origin allowed
+	cors = &corsConfig{
+		allowedOrigins: DefaultAllowedOrigins,
+		allowedHeaders: DefaultAllowedHeaders,
+		allowedMethods: DefaultAllowedMethods,
+	}
+}
+
 // WithAllowedOrigins is an option to supply allowed origins in CORS.
 func WithAllowedOrigins(origins []string) ServerConfigOption {
 	return func(cc *corsConfig, _ *runtime.ServeMux) {
@@ -123,14 +135,39 @@ func WithAdditionalHandler(method string, path string, h runtime.HandlerFunc) Se
 	}
 }
 
-func init() {
-	log = logrus.WithField("component", "rest")
-
-	// initialize the CORS config with restrictive default values, e.g. no origin allowed
-	cors = &corsConfig{
-		allowedOrigins: DefaultAllowedOrigins,
-		allowedHeaders: DefaultAllowedHeaders,
-		allowedMethods: DefaultAllowedMethods,
+// WithEmbeddedOAuth2Server configures our REST gateway to include an embedded OAuth 2.0
+// authorization server with the given parameters. This server can be used to authenticate
+// and authorize API clients, such as our own micro-services as well as users logging in
+// through the UI.
+//
+// For the various options to configure the OAuth 2.0 server, please refer to
+// https://pkg.go.dev/github.com/oxisto/oauth2go#AuthorizationServerOption.
+//
+// In production scenarios, the usage of a dedicated authentication and authorization server is
+// recommended.
+func WithEmbeddedOAuth2Server(keyPath string, keyPassword string, saveOnCreate bool, opts ...oauth2.AuthorizationServerOption) ServerConfigOption {
+	return func(cc *corsConfig, sm *runtime.ServeMux) {
+		opts = append(opts, oauth2.WithSigningKeysFunc(func() map[int]*ecdsa.PrivateKey {
+			return auth.LoadSigningKeys(keyPath, keyPassword, saveOnCreate)
+		}))
+
+		authSrv := oauth2.NewServer("", opts...)
+
+		WithAdditionalHandler("GET", "/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
+			authSrv.Handler.(*http.ServeMux).ServeHTTP(w, r)
+		})(cc, sm)
+		WithAdditionalHandler("GET", "/login", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
+			authSrv.Handler.(*http.ServeMux).ServeHTTP(w, r)
+		})(cc, sm)
+		WithAdditionalHandler("GET", "/authorize", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
+			authSrv.Handler.(*http.ServeMux).ServeHTTP(w, r)
+		})(cc, sm)
+		WithAdditionalHandler("POST", "/login", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
+			authSrv.Handler.(*http.ServeMux).ServeHTTP(w, r)
+		})(cc, sm)
+		WithAdditionalHandler("POST", "/token", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
+			authSrv.Handler.(*http.ServeMux).ServeHTTP(w, r)
+		})(cc, sm)
 	}
 }
 
@@ -140,9 +177,7 @@ func RunServer(ctx context.Context, grpcPort int, httpPort int, serverOpts ...Se
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 
-	mux := runtime.NewServeMux(
-		runtime.WithErrorHandler(service_auth.OAuthErrorHandler),
-	)
+	mux := runtime.NewServeMux()
 
 	for _, o := range serverOpts {
 		o(cors, mux)
@@ -150,10 +185,6 @@ func RunServer(ctx context.Context, grpcPort int, httpPort int, serverOpts ...Se
 
 	opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
 
-	if err := auth.RegisterAuthenticationHandlerFromEndpoint(ctx, mux, fmt.Sprintf("localhost:%d", grpcPort), opts); err != nil {
-		return fmt.Errorf("failed to connect to authentication gRPC service %w", err)
-	}
-
 	if err := discovery.RegisterDiscoveryHandlerFromEndpoint(ctx, mux, fmt.Sprintf("localhost:%d", grpcPort), opts); err != nil {
 		return fmt.Errorf("failed to connect to discovery gRPC service %w", err)
 	}
diff --git a/service/assessment/assessment.go b/service/assessment/assessment.go
index 6be5e958..047dc6ac 100644
--- a/service/assessment/assessment.go
+++ b/service/assessment/assessment.go
@@ -26,12 +26,6 @@
 package assessment
 
 import (
-	"clouditor.io/clouditor/api"
-	"clouditor.io/clouditor/api/assessment"
-	"clouditor.io/clouditor/api/evidence"
-	"clouditor.io/clouditor/api/orchestrator"
-	"clouditor.io/clouditor/policies"
-	service_orchestrator "clouditor.io/clouditor/service/orchestrator"
 	"context"
 	"encoding/json"
 	"errors"
@@ -40,6 +34,14 @@ import (
 	"sync"
 	"time"
 
+	"clouditor.io/clouditor/api"
+	"clouditor.io/clouditor/api/assessment"
+	"clouditor.io/clouditor/api/evidence"
+	"clouditor.io/clouditor/api/orchestrator"
+	"clouditor.io/clouditor/policies"
+	service_orchestrator "clouditor.io/clouditor/service/orchestrator"
+	"golang.org/x/oauth2/clientcredentials"
+
 	"github.com/google/uuid"
 	"github.com/sirupsen/logrus"
 	"google.golang.org/grpc"
@@ -121,10 +123,10 @@ func WithOrchestratorAddress(address string) ServiceOption {
 	}
 }
 
-// WithInternalAuthorizer is an option to use an authorizer to the internal Clouditor auth service.
-func WithInternalAuthorizer(address string, username string, password string, opts ...grpc.DialOption) ServiceOption {
+// WithOAuth2Authorizer is an option to use an OAuth 2.0 authorizer
+func WithOAuth2Authorizer(config *clientcredentials.Config) ServiceOption {
 	return func(s *Service) {
-		s.SetAuthorizer(api.NewInternalAuthorizerFromPassword(address, username, password, opts...))
+		s.SetAuthorizer(api.NewOAuthAuthorizerFromClientCredentials(config))
 	}
 }
 
diff --git a/service/auth/auth.go b/service/auth/auth.go
deleted file mode 100644
index c4e95115..00000000
--- a/service/auth/auth.go
+++ /dev/null
@@ -1,544 +0,0 @@
-// Copyright 2016-2020 Fraunhofer AISEC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-//           $$\                           $$\ $$\   $$\
-//           $$ |                          $$ |\__|  $$ |
-//  $$$$$$$\ $$ | $$$$$$\  $$\   $$\  $$$$$$$ |$$\ $$$$$$\    $$$$$$\   $$$$$$\
-// $$  _____|$$ |$$  __$$\ $$ |  $$ |$$  __$$ |$$ |\_$$  _|  $$  __$$\ $$  __$$\
-// $$ /      $$ |$$ /  $$ |$$ |  $$ |$$ /  $$ |$$ |  $$ |    $$ /  $$ |$$ | \__|
-// $$ |      $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$\ $$ |  $$ |$$ |
-// \$$$$$$\  $$ |\$$$$$   |\$$$$$   |\$$$$$$  |$$ |  \$$$   |\$$$$$   |$$ |
-//  \_______|\__| \______/  \______/  \_______|\__|   \____/  \______/ \__|
-//
-// This file is part of Clouditor Community Edition.
-
-package auth
-
-import (
-	"context"
-	"crypto/ecdsa"
-	"crypto/elliptic"
-	"crypto/rand"
-	"encoding/base64"
-	"errors"
-	"fmt"
-	"io/ioutil"
-	"net/http"
-	"os"
-	"os/user"
-	"path/filepath"
-	"strings"
-	"time"
-
-	"clouditor.io/clouditor/persistence/inmemory"
-
-	"clouditor.io/clouditor/api/auth"
-	"clouditor.io/clouditor/persistence"
-	argon2 "github.com/alexedwards/argon2id"
-	"github.com/golang-jwt/jwt/v4"
-	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
-	"github.com/sirupsen/logrus"
-	"google.golang.org/grpc/codes"
-	"google.golang.org/grpc/status"
-	"google.golang.org/protobuf/types/known/timestamppb"
-)
-
-var log *logrus.Entry
-
-func init() {
-	log = logrus.WithField("component", "auth")
-}
-
-const (
-	// DefaultApiKeySaveOnCreate specifies whether a created API key will be saved. This is useful to turn of in unit tests, where
-	// we only want a temporary key.
-	DefaultApiKeySaveOnCreate = true
-
-	// DefaultApiKeyPassword is the default password to protect the API key
-	DefaultApiKeyPassword = "changeme"
-
-	// DefaultApiKeyPath is the default path for the API private key
-	DefaultApiKeyPath = "~/.clouditor/api.key"
-
-	// DefaultKeyID specifies the default Key ID used in the JWKS of the authentication service
-	DefaultKeyID = "1"
-
-	// DefaultIssuer specifies the default issuer of the issued tokens.
-	DefaultIssuer = "clouditor"
-
-	// DefaultPeriodOfValidity specifies the default period of validity of a token
-	DefaultPeriodOfValidity = time.Hour * 24
-)
-
-// Service is an implementation of the gRPC Authentication service
-type Service struct {
-	auth.UnimplementedAuthenticationServer
-
-	config struct {
-		keySaveOnCreate bool
-		keyPath         string
-		keyPassword     string
-	}
-
-	apiKey *ecdsa.PrivateKey
-
-	storage persistence.Storage
-}
-
-// UserClaims extend jwt.StandardClaims with more detailed claims about a user
-type UserClaims struct {
-	jwt.RegisteredClaims
-	FullName string `json:"full_name"`
-	EMail    string `json:"email"`
-}
-
-// ServiceOption is a functional option type to configure the authentication service.
-type ServiceOption func(s *Service)
-
-// WithApiKeyPath is an option to configure the path for the API key.
-func WithApiKeyPath(path string) ServiceOption {
-	return func(s *Service) {
-		s.config.keyPath = path
-	}
-}
-
-// WithApiKeyPassword is an option to configure the password that is used to protect the API key.
-func WithApiKeyPassword(password string) ServiceOption {
-	return func(s *Service) {
-		s.config.keyPassword = password
-	}
-}
-
-// WithApiKeySaveOnCreate is an option to configure whether a key should be saved when it is created.
-func WithApiKeySaveOnCreate(saveOnCreate bool) ServiceOption {
-	return func(s *Service) {
-		s.config.keySaveOnCreate = saveOnCreate
-	}
-}
-
-// WithStorage is an option to set the storage. If not set, NewService will use inmemory storage.
-func WithStorage(storage persistence.Storage) ServiceOption {
-	return func(s *Service) {
-		s.storage = storage
-	}
-}
-
-// NewService creates a new Service representing an authentication service.
-func NewService(opts ...ServiceOption) *Service {
-	var err error
-
-	s := &Service{
-		config: struct {
-			keySaveOnCreate bool
-			keyPath         string
-			keyPassword     string
-		}{
-			keySaveOnCreate: DefaultApiKeySaveOnCreate,
-			keyPath:         DefaultApiKeyPath,
-			keyPassword:     DefaultApiKeyPassword,
-		},
-	}
-
-	// Apply options
-	for _, o := range opts {
-		o(s)
-	}
-
-	// Default to an in-memory storage, if nothing was explicitly set
-	if s.storage == nil {
-		s.storage, err = inmemory.NewStorage()
-		if err != nil {
-			log.Errorf("Could not initialize in-memory storage: %v", err)
-		}
-	}
-
-	// Load the key
-	s.apiKey, err = loadApiKey(s.config.keyPath, []byte(s.config.keyPassword))
-
-	// Recover from an error, so that we have at least a temporary key
-	if err != nil {
-		s.recoverFromLoadApiKeyError(err, s.config.keyPath == DefaultApiKeyPath)
-	}
-
-	// Lets clear out some sensitive things for slightly more security
-	s.config.keyPassword = ""
-
-	return s
-}
-
-// loadApiKey loads an ecdsa.PrivateKey from a path. The key must in PEM format and protected by
-// a password using PKCS#8 with PBES2.
-func loadApiKey(path string, password []byte) (key *ecdsa.PrivateKey, err error) {
-	var (
-		keyFile string
-	)
-
-	keyFile, err = expandPath(path)
-	if err != nil {
-		return nil, fmt.Errorf("error while expanding path: %w", err)
-	}
-
-	if _, err = os.Stat(keyFile); os.IsNotExist(err) {
-		return nil, fmt.Errorf("file does not exist (yet): %w", err)
-	}
-
-	// Check, if we already have a persisted API key
-	f, err := os.OpenFile(keyFile, os.O_RDONLY, 0600)
-	if err != nil {
-		return nil, fmt.Errorf("error while opening the file: %w", err)
-	}
-	defer func() {
-		err := f.Close()
-		if err != nil {
-			log.Errorf("Error while closing file: %v", err)
-		}
-	}()
-
-	data, err := ioutil.ReadAll(f)
-	if err != nil {
-		return nil, fmt.Errorf("error while reading file content: %w", err)
-	}
-
-	key, err = ParseECPrivateKeyFromPEMWithPassword(data, password)
-	if err != nil {
-		return nil, fmt.Errorf("error while parsing private key: %w", err)
-	}
-
-	return key, nil
-}
-
-// saveApiKey saves an ecdsa.PrivateKey to a path. The key will be saved in PEM format and protected by
-// a password using PKCS#8 with PBES2.
-func saveApiKey(apiKey *ecdsa.PrivateKey, keyPath string, password string) (err error) {
-	keyPath, err = expandPath(keyPath)
-	if err != nil {
-		return fmt.Errorf("error while expanding path: %w", err)
-	}
-
-	// Check, if we already have a persisted API key
-	f, err := os.OpenFile(keyPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
-	if err != nil {
-		return fmt.Errorf("error while opening the file: %w", err)
-	}
-	defer f.Close()
-
-	data, err := MarshalECPrivateKeyWithPassword(apiKey, []byte(password))
-	if err != nil {
-		return fmt.Errorf("error while marshalling private key: %w", err)
-	}
-
-	_, err = f.Write(data)
-	if err != nil {
-		return fmt.Errorf("error while writing file content: %w", err)
-	}
-
-	return nil
-}
-
-// recoverFromLoadApiKeyError tries to recover from an error during key loading. We treat different errors diffently.
-// For example if the path is the default path and the error is os.ErrNotExist, this could be just the first start of Clouditor.
-// So we only treat this as an information that we will create a new key, which we will save, based on the config.
-//
-// If the user specifies a custom path and this one does not exist, we will report an error
-// here.
-func (s *Service) recoverFromLoadApiKeyError(err error, defaultPath bool) {
-	// In any case, create a new temporary API key
-	s.apiKey, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-
-	if defaultPath && errors.Is(err, os.ErrNotExist) {
-		log.Infof("API key does not exist at the default location yet. We will create a new one")
-
-		if s.config.keySaveOnCreate {
-			// Also save the key in this case, so we can load it next time
-			err = saveApiKey(s.apiKey, s.config.keyPath, s.config.keyPassword)
-
-			// Error while error handling, meh
-			if err != nil {
-				log.Errorf("Error while saving the new API key: %v", err)
-			}
-		}
-	} else if err != nil {
-		log.Errorf("Could not load key from file, continuing with a temporary key: %v", err)
-	}
-}
-
-// Login handles a login request
-func (s Service) Login(_ context.Context, request *auth.LoginRequest) (response *auth.TokenResponse, err error) {
-	var (
-		result       bool
-		user         *auth.User
-		token        string
-		refreshToken string
-		expiry       = time.Now().Add(DefaultPeriodOfValidity)
-	)
-
-	if result, user, err = s.verifyLogin(request); err != nil {
-		// a returned error means, something has gone wrong
-		return nil, status.Errorf(codes.Internal, "error during login: %v", err)
-	}
-
-	if !result {
-		// authentication error
-		return nil, status.Error(codes.Unauthenticated, "login failed")
-	}
-
-	if token, err = issueToken(s.apiKey, user.Username, user.FullName, user.Email, expiry); err != nil {
-		return nil, status.Errorf(codes.Internal, "token issue failed: %v", err)
-	}
-
-	if refreshToken, err = issueRefreshToken(s.apiKey, user.Username); err != nil {
-		return nil, status.Errorf(codes.Internal, "refresh token issue failed: %v", err)
-	}
-
-	response = &auth.TokenResponse{
-		AccessToken:  token,
-		RefreshToken: refreshToken,
-		TokenType:    "Bearer",
-		Expiry:       timestamppb.New(expiry),
-	}
-
-	return response, nil
-}
-
-// Token aims to be a OAuth 2.0 compliant token endpoint. Currently, only the refresh_token grant type
-// is supported. This function only takes care of the application behavior, the actual implementation of
-// a HTTP endpoint around this functionality is provided by the gRPC gateway and is modified by the
-// OAuthErrorHandler function, which needs to be registered using runtime.WithErrorHandler in the gRPC
-// gateway.
-func (s *Service) Token(_ context.Context, req *auth.TokenRequest) (response *auth.TokenResponse, err error) {
-	var (
-		token  string
-		user   *auth.User
-		claims jwt.RegisteredClaims
-		expiry = time.Now().Add(DefaultPeriodOfValidity)
-	)
-
-	if req == nil {
-		return nil, status.Error(codes.InvalidArgument, "invalid_request")
-	}
-
-	// We are only supporting the refresh_token grant
-	if req.GrantType != "refresh_token" {
-		return nil, status.Error(codes.InvalidArgument, "unsupported_grant_type")
-	}
-
-	// Parse the refresh token
-	_, err = jwt.ParseWithClaims(req.RefreshToken, &claims, func(t *jwt.Token) (interface{}, error) {
-		return &s.apiKey.PublicKey, nil
-	})
-	if err != nil {
-		return nil, status.Error(codes.InvalidArgument, "invalid_grant")
-	}
-
-	err = s.storage.Get(&user, "username = ?", claims.Subject)
-	if errors.Is(err, persistence.ErrRecordNotFound) {
-		// User not found, we cannot successfully authenticate this refresh token
-		return nil, status.Error(codes.InvalidArgument, "invalid_grant")
-	} else if err != nil {
-		// A database connection error has occurred, return an an internal error but without any details
-		return nil, status.Errorf(codes.Internal, "internal database error")
-	}
-
-	// Issue a new token
-	if token, err = issueToken(s.apiKey, user.Username, user.FullName, user.Email, expiry); err != nil {
-		// An internal error occurred while creating the token, return an an internal error but without any details
-		return nil, status.Errorf(codes.Internal, "token issue failed")
-	}
-
-	// We do NOT return a refresh token, because we keep the old one. We only need to
-	// return one, if we change it
-	return &auth.TokenResponse{
-		AccessToken: token,
-		TokenType:   "Bearer",
-		Expiry:      timestamppb.New(expiry),
-	}, nil
-}
-
-// ListPublicKeys lists public keys in a JSON Web Key Set (JWKS). In our case we only return
-// a single key, which is our API key.
-func (s Service) ListPublicKeys(_ context.Context, _ *auth.ListPublicKeysRequest) (response *auth.ListPublicResponse, err error) {
-	response = &auth.ListPublicResponse{
-		Keys: []*auth.JsonWebKey{
-			{
-				Kid: DefaultKeyID,
-				Kty: "EC",
-				Crv: s.apiKey.Params().Name,
-				X:   base64.RawURLEncoding.EncodeToString(s.apiKey.X.Bytes()),
-				Y:   base64.RawURLEncoding.EncodeToString(s.apiKey.Y.Bytes()),
-			},
-		},
-	}
-
-	return response, nil
-}
-
-// verifyLogin compares the credentials supplied by request with the ones stored in the database.
-// It will return an error in err only if a database error or something else is occurred, this should be
-// returned to the user as an internal server error. For security reasons, if authentication failed, only
-// the result will be set to false, but no detailed error will be returned to the user.
-func (s *Service) verifyLogin(request *auth.LoginRequest) (result bool, user *auth.User, err error) {
-	var match bool
-
-	user = new(auth.User)
-
-	err = s.storage.Get(user, "username = ?", request.Username)
-
-	if errors.Is(err, persistence.ErrRecordNotFound) {
-		// user not found, set result to false, but hide the error
-		return false, nil, nil
-	} else if err != nil {
-		// a database connection error has occurred, return it
-		return false, nil, err
-	}
-
-	match, err = argon2.ComparePasswordAndHash(request.Password, user.Password)
-
-	if err != nil {
-		// some other error occurred, return it
-		return false, nil, err
-	}
-
-	if match {
-		return true, user, nil
-	} else {
-		return false, nil, nil
-	}
-}
-
-// hashPassword returns a hash of password using argon2id.
-func hashPassword(password string) (string, error) {
-	return argon2.CreateHash(password, &argon2.Params{
-		SaltLength:  16,
-		Memory:      65536,
-		KeyLength:   32,
-		Iterations:  3,
-		Parallelism: 6,
-	})
-}
-
-// CreateDefaultUser creates a default user in the database
-func (s *Service) CreateDefaultUser(username string, password string) error {
-	var (
-		err   error
-		count int64
-	)
-
-	count, err = s.storage.Count(&auth.User{})
-	if err != nil {
-		return fmt.Errorf("db error: %w", err)
-	} else if count == 0 {
-		hash, _ := hashPassword(password)
-
-		u := auth.User{
-			Username: username,
-			FullName: username,
-			Password: string(hash),
-		}
-
-		log.Infof("Creating default user %s\n", u.Username)
-
-		err = s.storage.Create(&u)
-		if err != nil {
-			return fmt.Errorf("db error: %w", err)
-		}
-	}
-
-	return nil
-}
-
-// issueToken issues a JWT-based token
-func issueToken(key *ecdsa.PrivateKey, subject string, fullName string, email string, expiry time.Time) (token string, err error) {
-	claims := jwt.NewWithClaims(jwt.SigningMethodES256,
-		&UserClaims{
-			FullName: fullName,
-			EMail:    email,
-			RegisteredClaims: jwt.RegisteredClaims{
-				ExpiresAt: jwt.NewNumericDate(expiry),
-				Issuer:    DefaultIssuer,
-				Subject:   subject,
-			}},
-	)
-	claims.Header["kid"] = DefaultKeyID
-
-	token, err = claims.SignedString(key)
-	return
-}
-
-func issueRefreshToken(key *ecdsa.PrivateKey, subject string) (token string, err error) {
-	claims := jwt.NewWithClaims(jwt.SigningMethodES256, &jwt.RegisteredClaims{
-		Subject: subject,
-		Issuer:  DefaultIssuer,
-	})
-
-	claims.Header["kid"] = DefaultKeyID
-
-	token, err = claims.SignedString(key)
-	return
-}
-
-func (s Service) GetPublicKey() *ecdsa.PublicKey {
-	return &s.apiKey.PublicKey
-}
-
-// AuthFuncOverride implements the ServiceAuthFuncOverride interface to override the AuthFunc for this service.
-// The reason is to actually disable authentication checking in the auth service, because functions such as login
-// need to be publicly available.
-func (Service) AuthFuncOverride(ctx context.Context, _ string) (context.Context, error) {
-	// No authentication needed for login functions, otherwise we could not log in
-	return ctx, nil
-}
-
-// expandPath expands a path that possible contains a tilde (~) character into the home directory
-// of the user
-func expandPath(path string) (out string, err error) {
-	var (
-		u *user.User
-	)
-
-	// Fetch the current user home directory
-	u, err = user.Current()
-	if err != nil {
-		return path, fmt.Errorf("could not find retrieve current user: %w", err)
-	}
-
-	if path == "~" {
-		return u.HomeDir, nil
-	} else if strings.HasPrefix(path, "~") {
-		// We only allow ~ at the beginning of the path
-		return filepath.Join(u.HomeDir, path[2:]), nil
-	}
-
-	return path, nil
-}
-
-// OAuthErrorHandler is an implementation of a runtime.ErrorHandlerFunc that customizes the behavior of
-// the gRPC gateway to be compliant to the OAuth 2.0 standard for the appropriate endpoints.
-func OAuthErrorHandler(c context.Context, sm *runtime.ServeMux, m runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) {
-	if status, ok := status.FromError(err); ok {
-		// For OAuth compliant messages, we need to directly return a JSON with an error, without any
-		// gRPC wrapping
-		if status.Message() == "unsupported_grant_type" || status.Message() == "invalid_grant" {
-			// Make sure, that error code is 400
-			w.WriteHeader(400)
-			_, err = w.Write([]byte(fmt.Sprintf(`{"error":"%s"}`, status.Message())))
-			if err != nil {
-				w.WriteHeader(500)
-			}
-
-			return
-		}
-	}
-
-	runtime.DefaultHTTPErrorHandler(c, sm, m, w, r, err)
-}
diff --git a/service/auth.go b/service/auth_middleware.go
similarity index 78%
rename from service/auth.go
rename to service/auth_middleware.go
index 36303458..0ae25b07 100644
--- a/service/auth.go
+++ b/service/auth_middleware.go
@@ -29,18 +29,12 @@ import (
 	"context"
 	"crypto/ecdsa"
 	"fmt"
-	"net"
 	"time"
 
-	"clouditor.io/clouditor/api/auth"
-	service_auth "clouditor.io/clouditor/service/auth"
-
 	"github.com/MicahParks/keyfunc"
 	"github.com/golang-jwt/jwt/v4"
-	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
 	grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
 	"github.com/sirupsen/logrus"
-	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 )
@@ -48,7 +42,7 @@ import (
 var log *logrus.Entry
 
 func init() {
-	log = logrus.WithField("component", "service-auth")
+	log = logrus.WithField("component", "auth-middleware")
 }
 
 type AuthConfig struct {
@@ -88,7 +82,7 @@ func WithPublicKey(publicKey *ecdsa.PublicKey) AuthOption {
 }
 
 // authContextKeyType is a key type that is used in context.WithValue to store the token info in the RPC context.
-// It should exlusivly be used with the value of AuthContextKey.
+// It should exclusively be used with the value of AuthContextKey.
 //
 // Why is this needed? To avoid conflicts, the string type should not be used directly but they should be type-aliased.
 type authContextKeyType string
@@ -161,35 +155,3 @@ func parseToken(token string, authConfig *AuthConfig) (jwt.Claims, error) {
 
 	return parsedToken.Claims, nil
 }
-
-// StartDedicatedAuthServer starts a gRPC server containing just the auth service
-func StartDedicatedAuthServer(address string, opts ...service_auth.ServiceOption) (sock net.Listener, server *grpc.Server, authService *service_auth.Service, err error) {
-	// create a new socket for gRPC communication
-	sock, err = net.Listen("tcp", address)
-	if err != nil {
-		return nil, nil, nil, fmt.Errorf("could not listen: %w", err)
-	}
-
-	authService = service_auth.NewService(opts...)
-	err = authService.CreateDefaultUser("clouditor", "clouditor")
-	if err != nil {
-		return nil, nil, nil, fmt.Errorf("could not create default user: %w", err)
-	}
-
-	authConfig := ConfigureAuth(WithPublicKey(authService.GetPublicKey()))
-
-	// We also add our authentication middleware, because we usually add additional service later
-	server = grpc.NewServer(
-		grpc_middleware.WithUnaryServerChain(
-			grpc_auth.UnaryServerInterceptor(authConfig.AuthFunc),
-		),
-	)
-	auth.RegisterAuthenticationServer(server, authService)
-
-	go func() {
-		// serve the gRPC socket
-		_ = server.Serve(sock)
-	}()
-
-	return sock, server, authService, nil
-}
diff --git a/service/discovery/discovery.go b/service/discovery/discovery.go
index d2cb3259..8eb59ca3 100644
--- a/service/discovery/discovery.go
+++ b/service/discovery/discovery.go
@@ -32,14 +32,14 @@ import (
 	"time"
 
 	"clouditor.io/clouditor/api"
+	"clouditor.io/clouditor/service/discovery/aws"
 	"clouditor.io/clouditor/service/discovery/azure"
 	"clouditor.io/clouditor/service/discovery/k8s"
+	"golang.org/x/oauth2/clientcredentials"
 	"google.golang.org/protobuf/types/known/timestamppb"
 
 	"github.com/google/uuid"
 
-	"clouditor.io/clouditor/service/discovery/aws"
-
 	"clouditor.io/clouditor/api/assessment"
 	"clouditor.io/clouditor/api/discovery"
 	"clouditor.io/clouditor/api/evidence"
@@ -102,10 +102,10 @@ func WithAssessmentAddress(address string) ServiceOption {
 	}
 }
 
-// WithInternalAuthorizer is an option to use an authorizer to the internal Clouditor auth service.
-func WithInternalAuthorizer(address string, username string, password string, opts ...grpc.DialOption) ServiceOption {
+// WithOAuth2Authorizer is an option to use an OAuth 2.0 authorizer
+func WithOAuth2Authorizer(config *clientcredentials.Config) ServiceOption {
 	return func(s *Service) {
-		s.SetAuthorizer(api.NewInternalAuthorizerFromPassword(address, username, password, opts...))
+		s.SetAuthorizer(api.NewOAuthAuthorizerFromClientCredentials(config))
 	}
 }
 
diff --git a/service/grpc.go b/service/grpc.go
new file mode 100644
index 00000000..5c911ff9
--- /dev/null
+++ b/service/grpc.go
@@ -0,0 +1,88 @@
+// Copyright 2022 Fraunhofer AISEC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//           $$\                           $$\ $$\   $$\
+//           $$ |                          $$ |\__|  $$ |
+//  $$$$$$$\ $$ | $$$$$$\  $$\   $$\  $$$$$$$ |$$\ $$$$$$\    $$$$$$\   $$$$$$\
+// $$  _____|$$ |$$  __$$\ $$ |  $$ |$$  __$$ |$$ |\_$$  _|  $$  __$$\ $$  __$$\
+// $$ /      $$ |$$ /  $$ |$$ |  $$ |$$ /  $$ |$$ |  $$ |    $$ /  $$ |$$ | \__|
+// $$ |      $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$\ $$ |  $$ |$$ |
+// \$$$$$$\  $$ |\$$$$$   |\$$$$$   |\$$$$$$  |$$ |  \$$$   |\$$$$$   |$$ |
+//  \_______|\__| \______/  \______/  \_______|\__|   \____/  \______/ \__|
+//
+// This file is part of Clouditor Community Edition.
+
+package service
+
+import (
+	"fmt"
+	"net"
+
+	"clouditor.io/clouditor/api/discovery"
+	"clouditor.io/clouditor/api/evidence"
+	"clouditor.io/clouditor/api/orchestrator"
+	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
+	grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
+	"google.golang.org/grpc"
+)
+
+type StartGRPCServerOption func(srv *grpc.Server)
+
+func WithOrchestrator(svc orchestrator.OrchestratorServer) StartGRPCServerOption {
+	return func(srv *grpc.Server) {
+		orchestrator.RegisterOrchestratorServer(srv, svc)
+	}
+}
+
+func WithEvidenceStore(svc evidence.EvidenceStoreServer) StartGRPCServerOption {
+	return func(srv *grpc.Server) {
+		evidence.RegisterEvidenceStoreServer(srv, svc)
+	}
+}
+
+func WithDiscovery(svc discovery.DiscoveryServer) StartGRPCServerOption {
+	return func(srv *grpc.Server) {
+		discovery.RegisterDiscoveryServer(srv, svc)
+	}
+}
+
+func StartGRPCServer(jwksURL string, opts ...StartGRPCServerOption) (sock net.Listener, srv *grpc.Server, err error) {
+	var addr = "127.0.0.1:0"
+
+	// create a new socket for gRPC communication
+	sock, err = net.Listen("tcp", addr)
+	if err != nil {
+		return nil, nil, fmt.Errorf("could not listen: %w", err)
+	}
+
+	authConfig := ConfigureAuth(WithJWKSURL(jwksURL))
+
+	// We also add our authentication middleware, because we usually add additional service later
+	srv = grpc.NewServer(
+		grpc_middleware.WithUnaryServerChain(
+			grpc_auth.UnaryServerInterceptor(authConfig.AuthFunc),
+		),
+	)
+
+	for _, o := range opts {
+		o(srv)
+	}
+
+	go func() {
+		// serve the gRPC socket
+		_ = srv.Serve(sock)
+	}()
+
+	return sock, srv, nil
+}


Test patch:
diff --git a/api/authorizer_test.go b/api/authorizer_test.go
index 38a4c72d..cbf88c15 100644
--- a/api/authorizer_test.go
+++ b/api/authorizer_test.go
@@ -27,163 +27,124 @@ package api
 
 import (
 	"context"
-	"crypto/ecdsa"
-	"crypto/elliptic"
-	"crypto/rand"
+	"fmt"
+	"net"
+	"net/http"
 	"reflect"
 	"testing"
-	"time"
 
-	"clouditor.io/clouditor/api/auth"
-	"github.com/golang-jwt/jwt/v4"
-	"golang.org/x/oauth2"
-	"google.golang.org/grpc"
-	"google.golang.org/protobuf/types/known/timestamppb"
+	oauth2 "github.com/oxisto/oauth2go"
+	"github.com/stretchr/testify/assert"
+	"golang.org/x/oauth2/clientcredentials"
 )
 
-var (
-	tmpKey           *ecdsa.PrivateKey
-	mockAccessToken  string
-	mockRefreshToken string
-	mockExpiry       time.Time
-)
-
-func init() {
-	// Create a new temporary key
-	tmpKey, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+func Test_oauthAuthorizer_Token(t *testing.T) {
+	// start an embedded oauth server
+	srv := oauth2.NewServer(":0", oauth2.WithClient("client", "secret", ""))
 
-	// Create a refresh token
-	claims := jwt.NewWithClaims(jwt.SigningMethodES256, &jwt.RegisteredClaims{
-		Subject: "clouditor",
-	})
+	ln, err := net.Listen("tcp", srv.Addr)
+	assert.NoError(t, err)
 
-	mockRefreshToken, _ = claims.SignedString(tmpKey)
+	port := ln.Addr().(*net.TCPAddr).Port
 
-	mockExpiry = time.Now().Add(time.Hour * 1).Truncate(time.Second).UTC()
-	// Create an access token
-	claims = jwt.NewWithClaims(jwt.SigningMethodES256, &jwt.RegisteredClaims{
-		Subject:   "clouditor",
-		ExpiresAt: jwt.NewNumericDate(mockExpiry),
-	})
+	go func() {
+		err = srv.Serve(ln)
+		if err != http.ErrServerClosed {
+			assert.NoError(t, err)
+		}
+	}()
 
-	mockAccessToken, _ = claims.SignedString(tmpKey)
-}
+	defer func() {
+		err = srv.Close()
+		if err != http.ErrServerClosed {
+			assert.NoError(t, err)
+		}
+	}()
 
-func TestInternalAuthorizer_Token(t *testing.T) {
 	type fields struct {
-		authURL     string
-		grpcOptions []grpc.DialOption
-		username    string
-		password    string
-		client      auth.AuthenticationClient
-		conn        grpc.ClientConnInterface
-		token       *oauth2.Token
+		TokenSource oauth2.TokenSource
+	}
+	type args struct {
+		refreshToken string
 	}
 	tests := []struct {
-		name    string
-		fields  fields
-		want    *oauth2.Token
-		wantErr bool
+		name     string
+		fields   fields
+		args     args
+		wantResp assert.ValueAssertionFunc
+		wantErr  bool
 	}{
 		{
-			name: "Fetch access token with refresh token",
-			fields: fields{
-				client: &mockAuthClient{},
-				conn:   &mockConn{},
-				token: &oauth2.Token{
-					RefreshToken: mockRefreshToken,
-				},
-			},
-			want: &oauth2.Token{
-				AccessToken: mockAccessToken,
-				Expiry:      mockExpiry,
-				TokenType:   "Bearer",
-			},
-		},
-		{
-			name: "Fetch access token with username",
-			fields: fields{
-				client:   &mockAuthClient{},
-				conn:     &mockConn{},
-				username: "mock",
-				password: "mock",
-			},
-			want: &oauth2.Token{
-				AccessToken: mockAccessToken,
-				Expiry:      mockExpiry,
-				TokenType:   "Bearer",
-			},
-		},
-		{
-			name: "Token still valid",
+			name: "fetch token without refresh token",
 			fields: fields{
-				client: &mockAuthClient{},
-				conn:   &mockConn{},
-				token: &oauth2.Token{
-					AccessToken: mockAccessToken,
-					TokenType:   "Bearer",
-					Expiry:      mockExpiry,
-				},
+				TokenSource: oauth2.ReuseTokenSource(nil,
+					(&clientcredentials.Config{
+						ClientID:     "client",
+						ClientSecret: "secret",
+						TokenURL:     fmt.Sprintf("http://localhost:%d/token", port),
+					}).TokenSource(context.Background()),
+				),
 			},
-			want: &oauth2.Token{
-				AccessToken: mockAccessToken,
-				Expiry:      mockExpiry,
-				TokenType:   "Bearer",
+			wantResp: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
+				token, ok := i1.(*oauth2.Token)
+				assert.True(t, ok)
+				assert.NotNil(tt, token)
+
+				return assert.NotEmpty(tt, token.AccessToken)
 			},
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			i := &internalAuthorizer{
-				authURL:     tt.fields.authURL,
-				grpcOptions: tt.fields.grpcOptions,
-				username:    tt.fields.username,
-				password:    tt.fields.password,
-				client:      tt.fields.client,
-				conn:        tt.fields.conn,
-				token:       tt.fields.token,
+			o := &oauthAuthorizer{
+				TokenSource: tt.fields.TokenSource,
 			}
-			got, err := i.Token()
+
+			gotResp, err := o.Token()
 			if (err != nil) != tt.wantErr {
-				t.Errorf("InternalAuthorizer.Token() error = %v, wantErr %v", err, tt.wantErr)
+				t.Errorf("oauthAuthorizer.fetchToken() error = %v, wantErr %v", err, tt.wantErr)
 				return
 			}
-			if !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("InternalAuthorizer.Token() = %v, want %v", got, tt.want)
+
+			if tt.wantResp != nil {
+				tt.wantResp(t, gotResp, tt.args.refreshToken)
 			}
 		})
 	}
 }
 
-type mockAuthClient struct{}
-
-func (mockAuthClient) Login(_ context.Context, _ *auth.LoginRequest, _ ...grpc.CallOption) (*auth.TokenResponse, error) {
-	return &auth.TokenResponse{
-		AccessToken: mockAccessToken,
-		TokenType:   "Bearer",
-		Expiry:      timestamppb.New(mockExpiry),
-	}, nil
-}
-
-func (mockAuthClient) Token(_ context.Context, _ *auth.TokenRequest, _ ...grpc.CallOption) (*auth.TokenResponse, error) {
-	return &auth.TokenResponse{
-		AccessToken: mockAccessToken,
-		TokenType:   "Bearer",
-		Expiry:      timestamppb.New(mockExpiry),
-	}, nil
-}
-
-func (mockAuthClient) ListPublicKeys(_ context.Context, _ *auth.ListPublicKeysRequest, _ ...grpc.CallOption) (*auth.ListPublicResponse, error) {
-	return nil, nil
-}
-
-type mockConn struct{}
+func TestNewOAuthAuthorizerFromClientCredentials(t *testing.T) {
+	var config = clientcredentials.Config{
+		ClientID:     "client",
+		ClientSecret: "secret",
+		TokenURL:     "/token",
+	}
 
-func (mockConn) Invoke(_ context.Context, _ string, _ interface{}, _ interface{}, _ ...grpc.CallOption) error {
-	return nil
-}
+	type args struct {
+		config *clientcredentials.Config
+	}
+	tests := []struct {
+		name string
+		args args
+		want Authorizer
+	}{
+		{
+			name: "new",
+			args: args{
+				&config,
+			},
+			want: &oauthAuthorizer{
+				TokenSource: oauth2.ReuseTokenSource(nil, config.TokenSource(context.Background())),
+			},
+		},
+	}
 
-func (mockConn) NewStream(_ context.Context, _ *grpc.StreamDesc, _ string, _ ...grpc.CallOption) (grpc.ClientStream, error) {
-	return nil, nil
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := NewOAuthAuthorizerFromClientCredentials(tt.args.config); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("NewOAuthAuthorizerFromClientCredentials() = %v, want %v", got, tt.want)
+			}
+		})
+	}
 }
diff --git a/cli/commands/assessmentresult/assessment_result_test.go b/cli/commands/assessmentresult/assessment_result_test.go
index f89a558c..9983c789 100644
--- a/cli/commands/assessmentresult/assessment_result_test.go
+++ b/cli/commands/assessmentresult/assessment_result_test.go
@@ -29,23 +29,17 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"net"
 	"os"
 	"testing"
 
-	"clouditor.io/clouditor/service"
-
 	"clouditor.io/clouditor/api/assessment"
 	"clouditor.io/clouditor/api/orchestrator"
 	"clouditor.io/clouditor/cli"
-	"clouditor.io/clouditor/cli/commands/login"
+	"clouditor.io/clouditor/internal/testutil/clitest"
+	"clouditor.io/clouditor/service"
 	service_orchestrator "clouditor.io/clouditor/service/orchestrator"
 
-	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
-	"google.golang.org/grpc"
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/types/known/structpb"
 	"google.golang.org/protobuf/types/known/timestamppb"
@@ -53,28 +47,16 @@ import (
 
 func TestMain(m *testing.M) {
 	var (
-		sock                net.Listener
-		server              *grpc.Server
-		orchestratorService *service_orchestrator.Service
-
+		svc *service_orchestrator.Service
 		err error
-		dir string
 	)
 
-	err = os.Chdir("../../../")
-	if err != nil {
-		panic(err)
-	}
+	clitest.AutoChdir()
 
-	orchestratorService = service_orchestrator.NewService()
+	svc = service_orchestrator.NewService()
 
-	sock, server, _, err = service.StartDedicatedAuthServer(":0")
-	if err != nil {
-		panic(err)
-	}
-	// Store an assessment result that output of CMD 'list' is not empty
-	orchestrator.RegisterOrchestratorServer(server, orchestratorService)
-	_, err = orchestratorService.StoreAssessmentResult(context.TODO(), &orchestrator.StoreAssessmentResultRequest{
+	// Store an assessment result so that output of CMD 'list' is not empty
+	_, err = svc.StoreAssessmentResult(context.TODO(), &orchestrator.StoreAssessmentResultRequest{
 		Result: &assessment.AssessmentResult{
 			Id:         "11111111-1111-1111-1111-111111111111",
 			MetricId:   "assessmentResultMetricID",
@@ -86,34 +68,11 @@ func TestMain(m *testing.M) {
 				Operator:    "operator",
 				IsDefault:   true,
 			}}})
-
 	if err != nil {
 		panic(err)
 	}
 
-	defer func(sock net.Listener) {
-		err = sock.Close()
-		if err != nil {
-			panic(err)
-		}
-	}(sock)
-	defer server.Stop()
-
-	dir, err = ioutil.TempDir(os.TempDir(), ".clouditor")
-	if err != nil {
-		panic(err)
-	}
-
-	viper.Set("username", "clouditor")
-	viper.Set("password", "clouditor")
-	viper.Set("session-directory", dir)
-
-	cmd := login.NewLoginCommand()
-	err = cmd.RunE(nil, []string{fmt.Sprintf("localhost:%d", sock.Addr().(*net.TCPAddr).Port)})
-	if err != nil {
-		panic(err)
-	}
-	defer os.Exit(m.Run())
+	os.Exit(clitest.RunCLITest(m, service.WithOrchestrator(svc)))
 }
 
 func TestAddCommands(t *testing.T) {
diff --git a/cli/commands/cloud/cloud_test.go b/cli/commands/cloud/cloud_test.go
index 100453d5..02d444f5 100644
--- a/cli/commands/cloud/cloud_test.go
+++ b/cli/commands/cloud/cloud_test.go
@@ -29,7 +29,6 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"io/ioutil"
 	"net"
 	"os"
 	"testing"
@@ -38,9 +37,11 @@ import (
 
 	"clouditor.io/clouditor/api/orchestrator"
 	"clouditor.io/clouditor/cli"
-	"clouditor.io/clouditor/cli/commands/login"
+	"clouditor.io/clouditor/internal/testutil"
+	"clouditor.io/clouditor/internal/testutil/clitest"
 	service_orchestrator "clouditor.io/clouditor/service/orchestrator"
 
+	oauth2 "github.com/oxisto/oauth2go"
 	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
 	"google.golang.org/grpc"
@@ -49,10 +50,7 @@ import (
 )
 
 func TestMain(m *testing.M) {
-	err := os.Chdir("../../../")
-	if err != nil {
-		panic(err)
-	}
+	clitest.AutoChdir()
 
 	defer os.Exit(m.Run())
 }
@@ -67,13 +65,18 @@ func TestNewCloudCommand(t *testing.T) {
 func TestRegisterCloudServiceCommand(t *testing.T) {
 	var (
 		response orchestrator.CloudService
+		svc      *service_orchestrator.Service
+		tmpDir   string
+		auth     *oauth2.AuthorizationServer
+		srv      *grpc.Server
 
 		err error
 		b   bytes.Buffer
 	)
-	_, server, sock := startServer()
-	defer sock.Close()
-	defer server.Stop()
+
+	svc = service_orchestrator.NewService()
+	tmpDir, auth, srv = startServer(service.WithOrchestrator(svc))
+	defer cleanup(tmpDir, srv, auth)
 
 	cli.Output = &b
 
@@ -91,15 +94,20 @@ func TestRegisterCloudServiceCommand(t *testing.T) {
 func TestListCloudServicesCommand(t *testing.T) {
 	var (
 		response orchestrator.ListCloudServicesResponse
+		svc      *service_orchestrator.Service
+		tmpDir   string
+		auth     *oauth2.AuthorizationServer
+		srv      *grpc.Server
 
 		err error
 		b   bytes.Buffer
 	)
-	orchestratorService, server, sock := startServer()
-	defer sock.Close()
-	defer server.Stop()
 
-	_, err = orchestratorService.CreateDefaultTargetCloudService()
+	svc = service_orchestrator.NewService()
+	tmpDir, auth, srv = startServer(service.WithOrchestrator(svc))
+	defer cleanup(tmpDir, srv, auth)
+
+	_, err = svc.CreateDefaultTargetCloudService()
 	assert.NoError(t, err)
 
 	cli.Output = &b
@@ -119,15 +127,21 @@ func TestGetCloudServiceCommand(t *testing.T) {
 	var (
 		response orchestrator.CloudService
 		target   *orchestrator.CloudService
+		svc      *service_orchestrator.Service
+		tmpDir   string
+		auth     *oauth2.AuthorizationServer
+		srv      *grpc.Server
 
 		err error
 		b   bytes.Buffer
 	)
-	orchestratorService, server, sock := startServer()
-	defer sock.Close()
-	defer server.Stop()
 
-	target, err = orchestratorService.CreateDefaultTargetCloudService()
+	svc = service_orchestrator.NewService()
+	tmpDir, auth, srv = startServer(service.WithOrchestrator(svc))
+	defer cleanup(tmpDir, srv, auth)
+
+	target, err = svc.CreateDefaultTargetCloudService()
+
 	fmt.Println("target:", target)
 	// target should be non-nil since it has been newly created
 	assert.NotNil(t, target)
@@ -150,15 +164,20 @@ func TestRemoveCloudServicesCommand(t *testing.T) {
 	var (
 		response emptypb.Empty
 		target   *orchestrator.CloudService
+		svc      *service_orchestrator.Service
+		tmpDir   string
+		auth     *oauth2.AuthorizationServer
+		srv      *grpc.Server
 
 		err error
 		b   bytes.Buffer
 	)
-	orchestratorService, server, sock := startServer()
-	defer sock.Close()
-	defer server.Stop()
 
-	target, err = orchestratorService.CreateDefaultTargetCloudService()
+	svc = service_orchestrator.NewService()
+	tmpDir, auth, srv = startServer(service.WithOrchestrator(svc))
+	defer cleanup(tmpDir, srv, auth)
+
+	target, err = svc.CreateDefaultTargetCloudService()
 	assert.NoError(t, err)
 
 	cli.Output = &b
@@ -173,7 +192,7 @@ func TestRemoveCloudServicesCommand(t *testing.T) {
 	assert.NoError(t, err)
 
 	// Re-create default service
-	_, err = orchestratorService.CreateDefaultTargetCloudService()
+	_, err = svc.CreateDefaultTargetCloudService()
 
 	assert.NoError(t, err)
 }
@@ -182,18 +201,24 @@ func TestUpdateCloudServiceCommand(t *testing.T) {
 	var (
 		response orchestrator.CloudService
 		target   *orchestrator.CloudService
+		svc      *service_orchestrator.Service
+		tmpDir   string
+		auth     *oauth2.AuthorizationServer
+		srv      *grpc.Server
 
 		err error
 		b   bytes.Buffer
 	)
+
 	const (
 		notDefault = "not_default"
 	)
-	orchestratorService, server, sock := startServer()
-	defer sock.Close()
-	defer server.Stop()
 
-	target, err = orchestratorService.CreateDefaultTargetCloudService()
+	svc = service_orchestrator.NewService()
+	tmpDir, auth, srv = startServer(service.WithOrchestrator(svc))
+	defer cleanup(tmpDir, srv, auth)
+
+	target, err = svc.CreateDefaultTargetCloudService()
 	assert.NoError(t, err)
 
 	cli.Output = &b
@@ -217,15 +242,20 @@ func TestUpdateCloudServiceCommand(t *testing.T) {
 func TestGetMetricConfiguration(t *testing.T) {
 	var (
 		target *orchestrator.CloudService
+		svc    *service_orchestrator.Service
+		tmpDir string
+		auth   *oauth2.AuthorizationServer
+		srv    *grpc.Server
 
 		err error
 		b   bytes.Buffer
 	)
-	orchestratorService, server, sock := startServer()
-	defer sock.Close()
-	defer server.Stop()
 
-	target, err = orchestratorService.CreateDefaultTargetCloudService()
+	svc = service_orchestrator.NewService()
+	tmpDir, auth, srv = startServer(service.WithOrchestrator(svc))
+	defer cleanup(tmpDir, srv, auth)
+
+	target, err = svc.CreateDefaultTargetCloudService()
 	assert.NoError(t, err)
 	// target should be not nil since there are no stored cloud services yet
 	assert.NotNil(t, target)
@@ -233,7 +263,7 @@ func TestGetMetricConfiguration(t *testing.T) {
 	cli.Output = &b
 
 	// create a new target service
-	target, err = orchestratorService.RegisterCloudService(context.TODO(), &orchestrator.RegisterCloudServiceRequest{Service: &orchestrator.CloudService{Name: "myservice"}})
+	target, err = svc.RegisterCloudService(context.TODO(), &orchestrator.RegisterCloudServiceRequest{Service: &orchestrator.CloudService{Name: "myservice"}})
 
 	assert.NotNil(t, target)
 	assert.NoError(t, err)
@@ -247,34 +277,39 @@ func TestGetMetricConfiguration(t *testing.T) {
 // startServer starts a gRPC server with an orchestrator and auth service. We don't do it in TestMain since you
 // can only register a service - once before server.serve(). And we do need to add new Orchestrator service because
 // the DB won't be reset otherwise.
-func startServer() (orchestratorService *service_orchestrator.Service, server *grpc.Server, sock net.Listener) {
+func startServer(opts ...service.StartGRPCServerOption) (tmpDir string, auth *oauth2.AuthorizationServer, srv *grpc.Server) {
 	var (
-		err error
-		dir string
+		err      error
+		grpcPort int
+		authPort int
+		sock     net.Listener
 	)
 
-	orchestratorService = service_orchestrator.NewService()
-
-	sock, server, _, err = service.StartDedicatedAuthServer(":0")
+	auth, authPort, err = testutil.StartAuthenticationServer()
 	if err != nil {
 		panic(err)
 	}
-	orchestrator.RegisterOrchestratorServer(server, orchestratorService)
 
-	dir, err = ioutil.TempDir(os.TempDir(), ".clouditor")
+	sock, srv, err = service.StartGRPCServer(testutil.JWKSURL(authPort), opts...)
 	if err != nil {
 		panic(err)
 	}
 
-	viper.Set("username", "clouditor")
-	viper.Set("password", "clouditor")
-	viper.Set("session-directory", dir)
+	grpcPort = sock.Addr().(*net.TCPAddr).Port
 
-	cmd := login.NewLoginCommand()
-	err = cmd.RunE(nil, []string{fmt.Sprintf("localhost:%d", sock.Addr().(*net.TCPAddr).Port)})
+	tmpDir, err = clitest.PrepareSession(authPort, auth, fmt.Sprintf("localhost:%d", grpcPort))
 	if err != nil {
 		panic(err)
 	}
 
 	return
 }
+
+func cleanup(tmpDir string, srv *grpc.Server, auth *oauth2.AuthorizationServer) {
+	// Remove temporary session directory
+	os.RemoveAll(tmpDir)
+
+	srv.Stop()
+
+	auth.Close()
+}
diff --git a/cli/commands/evidence/evidence_test.go b/cli/commands/evidence/evidence_test.go
index e841af77..97130bb2 100644
--- a/cli/commands/evidence/evidence_test.go
+++ b/cli/commands/evidence/evidence_test.go
@@ -29,23 +29,17 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"net"
 	"os"
 	"testing"
 
-	"clouditor.io/clouditor/service"
-
 	"clouditor.io/clouditor/api/evidence"
 	"clouditor.io/clouditor/cli"
-	"clouditor.io/clouditor/cli/commands/login"
-	service_evidenceStore "clouditor.io/clouditor/service/evidence"
-
+	"clouditor.io/clouditor/internal/testutil/clitest"
+	"clouditor.io/clouditor/service"
+	service_evidence "clouditor.io/clouditor/service/evidence"
 	"clouditor.io/clouditor/voc"
-	"github.com/spf13/viper"
+
 	"github.com/stretchr/testify/assert"
-	"google.golang.org/grpc"
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/types/known/structpb"
 	"google.golang.org/protobuf/types/known/timestamppb"
@@ -53,60 +47,23 @@ import (
 
 func TestMain(m *testing.M) {
 	var (
-		sock                 net.Listener
-		server               *grpc.Server
-		evidenceStoreService *service_evidenceStore.Service
-
+		svc *service_evidence.Service
 		err error
-		dir string
 	)
 
-	err = os.Chdir("../../../")
-	if err != nil {
-		panic(err)
-	}
+	svc = service_evidence.NewService()
 
-	evidenceStoreService = service_evidenceStore.NewService()
-
-	sock, server, _, err = service.StartDedicatedAuthServer(":0")
-	if err != nil {
-		panic(err)
-	}
-	evidence.RegisterEvidenceStoreServer(server, evidenceStoreService)
-	_, err = evidenceStoreService.StoreEvidence(context.TODO(), &evidence.StoreEvidenceRequest{Evidence: &evidence.Evidence{
+	_, err = svc.StoreEvidence(context.TODO(), &evidence.StoreEvidenceRequest{Evidence: &evidence.Evidence{
 		Id:        "11111111-1111-1111-1111-111111111111",
 		ToolId:    "mock",
 		Timestamp: timestamppb.Now(),
 		Resource:  toStruct(voc.VirtualMachine{Compute: &voc.Compute{CloudResource: &voc.CloudResource{ID: "my-resource-id", Type: []string{"VirtualMachine"}}}}),
 	}})
-
-	if err != nil {
-		panic(err)
-	}
-
-	defer func(sock net.Listener) {
-		err = sock.Close()
-		if err != nil {
-			panic(err)
-		}
-	}(sock)
-	defer server.Stop()
-
-	dir, err = ioutil.TempDir(os.TempDir(), ".clouditor")
 	if err != nil {
 		panic(err)
 	}
 
-	viper.Set("username", "clouditor")
-	viper.Set("password", "clouditor")
-	viper.Set("session-directory", dir)
-
-	cmd := login.NewLoginCommand()
-	err = cmd.RunE(nil, []string{fmt.Sprintf("localhost:%d", sock.Addr().(*net.TCPAddr).Port)})
-	if err != nil {
-		panic(err)
-	}
-	defer os.Exit(m.Run())
+	os.Exit(clitest.RunCLITest(m, service.WithEvidenceStore(svc)))
 }
 
 func TestAddCommands(t *testing.T) {
diff --git a/cli/commands/login/login_test.go b/cli/commands/login/login_test.go
index ce4edaac..9a71574b 100644
--- a/cli/commands/login/login_test.go
+++ b/cli/commands/login/login_test.go
@@ -29,11 +29,16 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net"
+	"net/http"
 	"os"
 	"testing"
+	"time"
 
+	"clouditor.io/clouditor/cli"
+	"clouditor.io/clouditor/internal/testutil"
 	"clouditor.io/clouditor/service"
 
+	oauth2 "github.com/oxisto/oauth2go"
 	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
 	"google.golang.org/grpc"
@@ -48,7 +53,7 @@ func TestMain(m *testing.M) {
 	var (
 		err error
 	)
-	sock, server, _, err = service.StartDedicatedAuthServer(":0")
+	sock, server, err = service.StartGRPCServer("")
 	if err != nil {
 		panic(err)
 	}
@@ -58,10 +63,16 @@ func TestMain(m *testing.M) {
 
 func TestLogin(t *testing.T) {
 	var (
-		err error
-		dir string
+		err      error
+		dir      string
+		verifier string
+		authSrv  *oauth2.AuthorizationServer
+		port     int
 	)
 
+	authSrv, port, err = testutil.StartAuthenticationServer()
+	assert.NoError(t, err)
+
 	defer sock.Close()
 	defer server.Stop()
 
@@ -69,11 +80,43 @@ func TestLogin(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotEmpty(t, dir)
 
-	viper.Set("username", "clouditor")
-	viper.Set("password", "clouditor")
-	viper.Set("session-directory", dir)
+	viper.Set(OAuth2ServerFlag, fmt.Sprintf("http://localhost:%d", port))
+	viper.Set(cli.SessionFolderFlag, dir)
+
+	verifier = "012345678901234567890123456789"
+	VerifierGenerator = func() string {
+		return verifier
+	}
+
+	// Issue a code that we can use in the callback
+	code := authSrv.IssueCode(oauth2.GenerateCodeChallenge(verifier))
 
 	cmd := NewLoginCommand()
-	err = cmd.RunE(nil, []string{fmt.Sprintf("localhost:%d", sock.Addr().(*net.TCPAddr).Port)})
-	assert.NoError(t, err)
+
+	// Because this potentially blocks, we need to wrap all of this in a timeout
+	// TODO(oxisto): This can be removed once https://github.com/golang/go/issues/48157 is fixed
+	timeout := time.After(5 * time.Second)
+	done := make(chan bool)
+
+	go func() {
+		// Simulate a callback
+		go func() {
+			// Wait for the callback server to be ready
+			<-callbackServerReady
+			_, err = http.Get(fmt.Sprintf("%s?code=%s", DefaultCallback, code))
+			if err != nil {
+				assert.NoError(t, err)
+			}
+		}()
+
+		err = cmd.RunE(nil, []string{fmt.Sprintf("localhost:%d", sock.Addr().(*net.TCPAddr).Port)})
+		assert.NoError(t, err)
+		done <- true
+	}()
+
+	select {
+	case <-timeout:
+		assert.Fail(t, "Did not finish in time")
+	case <-done:
+	}
 }
diff --git a/cli/commands/metric/metric_test.go b/cli/commands/metric/metric_test.go
index 4ed7cae4..400f5afa 100644
--- a/cli/commands/metric/metric_test.go
+++ b/cli/commands/metric/metric_test.go
@@ -27,68 +27,23 @@ package metric
 
 import (
 	"bytes"
-	"fmt"
-	"io/ioutil"
-	"net"
 	"os"
 	"testing"
 
-	"clouditor.io/clouditor/service"
-
 	"clouditor.io/clouditor/api/orchestrator"
 	"clouditor.io/clouditor/cli"
-	"clouditor.io/clouditor/cli/commands/login"
+	"clouditor.io/clouditor/internal/testutil/clitest"
+	"clouditor.io/clouditor/service"
 	service_orchestrator "clouditor.io/clouditor/service/orchestrator"
 
-	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
-	"google.golang.org/grpc"
 	"google.golang.org/protobuf/encoding/protojson"
 )
 
 func TestMain(m *testing.M) {
-	var (
-		sock                net.Listener
-		server              *grpc.Server
-		orchestratorService *service_orchestrator.Service
-
-		err error
-		dir string
-	)
-
-	err = os.Chdir("../../../")
-	if err != nil {
-		panic(err)
-	}
-
-	orchestratorService = service_orchestrator.NewService()
-
-	sock, server, _, err = service.StartDedicatedAuthServer(":0")
-	orchestrator.RegisterOrchestratorServer(server, orchestratorService)
-
-	if err != nil {
-		panic(err)
-	}
-
-	defer sock.Close()
-	defer server.Stop()
-
-	dir, err = ioutil.TempDir(os.TempDir(), ".clouditor")
-	if err != nil {
-		panic(err)
-	}
-
-	viper.Set("username", "clouditor")
-	viper.Set("password", "clouditor")
-	viper.Set("session-directory", dir)
-
-	cmd := login.NewLoginCommand()
-	err = cmd.RunE(nil, []string{fmt.Sprintf("localhost:%d", sock.Addr().(*net.TCPAddr).Port)})
-	if err != nil {
-		panic(err)
-	}
+	clitest.AutoChdir()
 
-	defer os.Exit(m.Run())
+	os.Exit(clitest.RunCLITest(m, service.WithOrchestrator(service_orchestrator.NewService())))
 }
 
 func TestListMetrics(t *testing.T) {
diff --git a/cli/commands/resource/resource_test.go b/cli/commands/resource/resource_test.go
index 0235f378..2bb71462 100644
--- a/cli/commands/resource/resource_test.go
+++ b/cli/commands/resource/resource_test.go
@@ -28,75 +28,26 @@ package resource
 import (
 	"bytes"
 	"fmt"
-	"io/ioutil"
-	"net"
 	"os"
 	"testing"
 	"time"
 
-	"clouditor.io/clouditor/service"
-
 	"clouditor.io/clouditor/api/discovery"
 	"clouditor.io/clouditor/cli"
-	"clouditor.io/clouditor/cli/commands/login"
+	"clouditor.io/clouditor/internal/testutil/clitest"
+	"clouditor.io/clouditor/service"
 	service_discovery "clouditor.io/clouditor/service/discovery"
-
 	"clouditor.io/clouditor/voc"
-	"github.com/spf13/viper"
+
 	"github.com/stretchr/testify/assert"
-	"google.golang.org/grpc"
 	"google.golang.org/protobuf/encoding/protojson"
 )
 
-var (
-	sock             net.Listener
-	server           *grpc.Server
-	discoveryService *service_discovery.Service
-)
-
 func TestMain(m *testing.M) {
-	var (
-		err error
-		dir string
-	)
-
-	err = os.Chdir("../../../../")
-	if err != nil {
-		panic(err)
-	}
-
-	discoveryService = service_discovery.NewService()
-	discoveryService.StartDiscovery(mockDiscoverer{testCase: 2})
+	svc := service_discovery.NewService()
+	svc.StartDiscovery(mockDiscoverer{testCase: 2})
 
-	sock, server, _, err = service.StartDedicatedAuthServer(":0")
-	if err != nil {
-		panic(err)
-	}
-	discovery.RegisterDiscoveryServer(server, discoveryService)
-
-	defer func(sock net.Listener) {
-		err = sock.Close()
-		if err != nil {
-			panic(err)
-		}
-	}(sock)
-	defer server.Stop()
-
-	dir, err = ioutil.TempDir(os.TempDir(), ".clouditor")
-	if err != nil {
-		panic(err)
-	}
-
-	viper.Set("username", "clouditor")
-	viper.Set("password", "clouditor")
-	viper.Set("session-directory", dir)
-
-	cmd := login.NewLoginCommand()
-	err = cmd.RunE(nil, []string{fmt.Sprintf("localhost:%d", sock.Addr().(*net.TCPAddr).Port)})
-	if err != nil {
-		panic(err)
-	}
-	defer os.Exit(m.Run())
+	os.Exit(clitest.RunCLITest(m, service.WithDiscovery(svc)))
 }
 
 func TestAddCommands(t *testing.T) {
@@ -130,7 +81,6 @@ func TestNewListCommand(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotNil(t, response)
 	assert.NotEmpty(t, response.Results.Values)
-
 }
 
 // Mocking code below is copied from clouditor.io/service/discovery
diff --git a/cli/commands/service/discovery/discovery_test.go b/cli/commands/service/discovery/discovery_test.go
index c5adf76e..1f61a74c 100644
--- a/cli/commands/service/discovery/discovery_test.go
+++ b/cli/commands/service/discovery/discovery_test.go
@@ -28,75 +28,26 @@ package discovery
 import (
 	"bytes"
 	"fmt"
-	"io/ioutil"
-	"net"
 	"os"
 	"testing"
 	"time"
 
-	"clouditor.io/clouditor/service"
-
 	"clouditor.io/clouditor/api/discovery"
 	"clouditor.io/clouditor/cli"
-	"clouditor.io/clouditor/cli/commands/login"
+	"clouditor.io/clouditor/internal/testutil/clitest"
+	"clouditor.io/clouditor/service"
 	service_discovery "clouditor.io/clouditor/service/discovery"
-
 	"clouditor.io/clouditor/voc"
-	"github.com/spf13/viper"
+
 	"github.com/stretchr/testify/assert"
-	"google.golang.org/grpc"
 	"google.golang.org/protobuf/encoding/protojson"
 )
 
-var (
-	sock             net.Listener
-	server           *grpc.Server
-	discoveryService *service_discovery.Service
-)
-
 func TestMain(m *testing.M) {
-	var (
-		err error
-		dir string
-	)
-
-	err = os.Chdir("../../../../")
-	if err != nil {
-		panic(err)
-	}
-
-	discoveryService = service_discovery.NewService()
-	discoveryService.StartDiscovery(mockDiscoverer{testCase: 2})
+	svc := service_discovery.NewService()
+	svc.StartDiscovery(mockDiscoverer{testCase: 2})
 
-	sock, server, _, err = service.StartDedicatedAuthServer(":0")
-	if err != nil {
-		panic(err)
-	}
-	discovery.RegisterDiscoveryServer(server, discoveryService)
-
-	defer func(sock net.Listener) {
-		err = sock.Close()
-		if err != nil {
-			panic(err)
-		}
-	}(sock)
-	defer server.Stop()
-
-	dir, err = ioutil.TempDir(os.TempDir(), ".clouditor")
-	if err != nil {
-		panic(err)
-	}
-
-	viper.Set("username", "clouditor")
-	viper.Set("password", "clouditor")
-	viper.Set("session-directory", dir)
-
-	cmd := login.NewLoginCommand()
-	err = cmd.RunE(nil, []string{fmt.Sprintf("localhost:%d", sock.Addr().(*net.TCPAddr).Port)})
-	if err != nil {
-		panic(err)
-	}
-	defer os.Exit(m.Run())
+	os.Exit(clitest.RunCLITest(m, service.WithDiscovery(svc)))
 }
 
 func TestAddCommands(t *testing.T) {
diff --git a/cli/commands/service/orchestrator/orchestrator_test.go b/cli/commands/service/orchestrator/orchestrator_test.go
index 97fa804a..087ba622 100644
--- a/cli/commands/service/orchestrator/orchestrator_test.go
+++ b/cli/commands/service/orchestrator/orchestrator_test.go
@@ -29,54 +29,35 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"net"
 	"os"
 	"testing"
 
+	"clouditor.io/clouditor/internal/testutil/clitest"
 	"clouditor.io/clouditor/service"
 
 	"clouditor.io/clouditor/api/assessment"
 	"clouditor.io/clouditor/api/orchestrator"
 	"clouditor.io/clouditor/cli"
-	"clouditor.io/clouditor/cli/commands/login"
 	service_orchestrator "clouditor.io/clouditor/service/orchestrator"
 
-	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
-	"google.golang.org/grpc"
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/types/known/structpb"
 	"google.golang.org/protobuf/types/known/timestamppb"
 )
 
-var (
-	sock                net.Listener
-	server              *grpc.Server
-	orchestratorService *service_orchestrator.Service
-)
-
 func TestMain(m *testing.M) {
 	var (
+		svc *service_orchestrator.Service
 		err error
-		dir string
 	)
 
-	err = os.Chdir("../../../../")
-	if err != nil {
-		panic(err)
-	}
+	clitest.AutoChdir()
 
-	orchestratorService = service_orchestrator.NewService()
+	svc = service_orchestrator.NewService()
 
-	sock, server, _, err = service.StartDedicatedAuthServer(":0")
-	if err != nil {
-		panic(err)
-	}
-	orchestrator.RegisterOrchestratorServer(server, orchestratorService)
-	// Store an assessment result that output of CMD 'list_results' is not empty
-	_, err = orchestratorService.StoreAssessmentResult(context.TODO(), &orchestrator.StoreAssessmentResultRequest{
+	// Store an assessment result so that output of CMD 'list' is not empty
+	_, err = svc.StoreAssessmentResult(context.TODO(), &orchestrator.StoreAssessmentResultRequest{
 		Result: &assessment.AssessmentResult{
 			Id:         "11111111-1111-1111-1111-111111111111",
 			MetricId:   "assessmentResultMetricID",
@@ -88,34 +69,11 @@ func TestMain(m *testing.M) {
 				Operator:    "operator",
 				IsDefault:   true,
 			}}})
-
 	if err != nil {
 		panic(err)
 	}
 
-	defer func(sock net.Listener) {
-		err = sock.Close()
-		if err != nil {
-			panic(err)
-		}
-	}(sock)
-	defer server.Stop()
-
-	dir, err = ioutil.TempDir(os.TempDir(), ".clouditor")
-	if err != nil {
-		panic(err)
-	}
-
-	viper.Set("username", "clouditor")
-	viper.Set("password", "clouditor")
-	viper.Set("session-directory", dir)
-
-	cmd := login.NewLoginCommand()
-	err = cmd.RunE(nil, []string{fmt.Sprintf("localhost:%d", sock.Addr().(*net.TCPAddr).Port)})
-	if err != nil {
-		panic(err)
-	}
-	defer os.Exit(m.Run())
+	os.Exit(clitest.RunCLITest(m, service.WithOrchestrator(svc)))
 }
 
 func TestAddCommands(t *testing.T) {
diff --git a/cli/commands/tool/tool_test.go b/cli/commands/tool/tool_test.go
index 2c7c2377..c1382554 100644
--- a/cli/commands/tool/tool_test.go
+++ b/cli/commands/tool/tool_test.go
@@ -26,67 +26,20 @@
 package tool
 
 import (
-	"fmt"
-	"io/ioutil"
-	"net"
 	"os"
 	"testing"
 
+	"clouditor.io/clouditor/internal/testutil/clitest"
 	"clouditor.io/clouditor/service"
-
-	"clouditor.io/clouditor/api/orchestrator"
-	"clouditor.io/clouditor/cli/commands/login"
 	service_orchestrator "clouditor.io/clouditor/service/orchestrator"
 
-	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
-	"google.golang.org/grpc"
-)
-
-var (
-	sock                net.Listener
-	server              *grpc.Server
-	orchestratorService *service_orchestrator.Service
 )
 
 func TestMain(m *testing.M) {
-	var (
-		err error
-		dir string
-	)
-
-	err = os.Chdir("../../../")
-	if err != nil {
-		panic(err)
-	}
-	orchestratorService = service_orchestrator.NewService()
-
-	sock, server, _, err = service.StartDedicatedAuthServer(":0")
-	orchestrator.RegisterOrchestratorServer(server, orchestratorService)
-
-	if err != nil {
-		panic(err)
-	}
-
-	defer sock.Close()
-	defer server.Stop()
-
-	dir, err = ioutil.TempDir(os.TempDir(), ".clouditor")
-	if err != nil {
-		panic(err)
-	}
-
-	viper.Set("username", "clouditor")
-	viper.Set("password", "clouditor")
-	viper.Set("session-directory", dir)
-
-	cmd := login.NewLoginCommand()
-	err = cmd.RunE(nil, []string{fmt.Sprintf("localhost:%d", sock.Addr().(*net.TCPAddr).Port)})
-	if err != nil {
-		panic(err)
-	}
-
-	os.Exit(m.Run())
+	clitest.AutoChdir()
+
+	os.Exit(clitest.RunCLITest(m, service.WithOrchestrator(service_orchestrator.NewService())))
 }
 
 func TestListTool(t *testing.T) {
diff --git a/cli/session_test.go b/cli/session_test.go
index 0aae6c72..d112b7b2 100644
--- a/cli/session_test.go
+++ b/cli/session_test.go
@@ -23,108 +23,54 @@
 //
 // This file is part of Clouditor Community Edition.
 
-package cli
+package cli_test
 
 import (
 	"context"
 	"fmt"
-	"io/ioutil"
-	"net"
 	"os"
+	"reflect"
 	"testing"
 
-	"clouditor.io/clouditor/api"
-	"clouditor.io/clouditor/api/auth"
 	"clouditor.io/clouditor/api/orchestrator"
+	"clouditor.io/clouditor/cli"
+	"clouditor.io/clouditor/internal/testutil/clitest"
 	"clouditor.io/clouditor/service"
-	service_auth "clouditor.io/clouditor/service/auth"
 	service_orchestrator "clouditor.io/clouditor/service/orchestrator"
 
-	"github.com/spf13/viper"
+	oauth2 "github.com/oxisto/oauth2go"
+	"github.com/spf13/cobra"
 	"github.com/stretchr/testify/assert"
-	"golang.org/x/oauth2"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 	"google.golang.org/protobuf/proto"
 )
 
-var (
-	sock   net.Listener
-	server *grpc.Server
-)
-
 func TestMain(m *testing.M) {
 	var (
+		svc *service_orchestrator.Service
 		err error
 	)
-	err = os.Chdir("../")
-	if err != nil {
-		panic(err)
-	}
 
-	s := service_orchestrator.NewService()
-	sock, server, _, err = service.StartDedicatedAuthServer(":0", service_auth.WithApiKeySaveOnCreate(false))
-	orchestrator.RegisterOrchestratorServer(server, s)
+	svc = service_orchestrator.NewService()
+	_, err = svc.CreateDefaultTargetCloudService()
 	if err != nil {
 		panic(err)
 	}
 
-	os.Exit(m.Run())
+	clitest.AutoChdir()
+
+	os.Exit(clitest.RunCLITest(m, service.WithOrchestrator(svc)))
 }
 
 func TestSession(t *testing.T) {
 	var (
 		err     error
-		session *Session
-		dir     string
+		session *cli.Session
 	)
-	defer sock.Close()
-	defer server.Stop()
-
-	dir, err = ioutil.TempDir(os.TempDir(), ".clouditor")
-	assert.NoError(t, err)
-	assert.NotEmpty(t, dir)
-
-	viper.Set("session-directory", dir)
-
-	session, err = NewSession(fmt.Sprintf("localhost:%d", sock.Addr().(*net.TCPAddr).Port))
-	if err != nil {
-		t.Fatalf("Failed to dial bufnet: %v", err)
-	}
-	defer session.Close()
 
-	assert.NoError(t, err)
-	assert.NotNil(t, session)
-	assert.Equal(t, dir, session.Folder)
-
-	client := auth.NewAuthenticationClient(session)
-
-	var response *auth.TokenResponse
-
-	// login with real user
-	response, err = client.Login(context.Background(), &auth.LoginRequest{Username: "clouditor", Password: "clouditor"})
-
-	assert.NoError(t, err)
-	assert.NotNil(t, response)
-	assert.NotEmpty(t, response.AccessToken)
-
-	// update the session
-	session.authorizer = api.NewInternalAuthorizerFromToken(
-		session.authorizer.AuthURL(),
-		&oauth2.Token{
-			AccessToken:  response.AccessToken,
-			TokenType:    response.TokenType,
-			RefreshToken: response.RefreshToken,
-			Expiry:       response.Expiry.AsTime(),
-		},
-	)
-
-	err = session.Save()
-
-	assert.NoError(t, err)
-
-	session, err = ContinueSession()
+	session, err = cli.ContinueSession()
 	assert.NoError(t, err)
 	assert.NotNil(t, session)
 
@@ -158,12 +104,7 @@ func TestSession_HandleResponse(t *testing.T) {
 				err: status.Errorf(codes.Internal, "internal error occurred!"),
 			},
 			wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
-				if err != nil {
-					return true
-				} else {
-					t.Errorf("Expected error.")
-					return false
-				}
+				return assert.Error(t, err)
 			},
 		},
 		{
@@ -173,20 +114,14 @@ func TestSession_HandleResponse(t *testing.T) {
 				err: fmt.Errorf("random error"),
 			},
 			wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
-				if err != nil {
-					return true
-				} else {
-					t.Errorf("Expected error.")
-					return false
-				}
+				return assert.Error(t, err)
 			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			s := &Session{
-				URL: tt.fields.URL,
-				//Token:      tt.fields.Token,
+			s := &cli.Session{
+				URL:        tt.fields.URL,
 				Folder:     tt.fields.Folder,
 				ClientConn: tt.fields.ClientConn,
 			}
@@ -195,8 +130,80 @@ func TestSession_HandleResponse(t *testing.T) {
 	}
 }
 
-// Test will fail due to no user input
-func TestPromptForLogin(t *testing.T) {
-	_, err := PromptForLogin()
-	assert.Error(t, err)
+func TestValidArgsGetMetrics(t *testing.T) {
+	type args struct {
+		in0        *cobra.Command
+		args       []string
+		toComplete string
+	}
+	tests := []struct {
+		name  string
+		args  args
+		want  assert.ValueAssertionFunc
+		want1 cobra.ShellCompDirective
+	}{
+		{
+			name: "some metrics",
+			args: args{
+				toComplete: "",
+			},
+			want: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
+				return assert.NotNil(tt, i1)
+			},
+			want1: cobra.ShellCompDirectiveNoFileComp,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, got1 := cli.ValidArgsGetMetrics(tt.args.in0, tt.args.args, tt.args.toComplete)
+
+			if tt.want != nil {
+				tt.want(t, got)
+			}
+
+			if !reflect.DeepEqual(got1, tt.want1) {
+				t.Errorf("ValidArgsGetMetrics() got1 = %v, want %v", got1, tt.want1)
+			}
+		})
+	}
+}
+
+func TestValidArgsGetCloudServices(t *testing.T) {
+	type args struct {
+		in0        *cobra.Command
+		args       []string
+		toComplete string
+	}
+	tests := []struct {
+		name  string
+		args  args
+		want  assert.ValueAssertionFunc
+		want1 cobra.ShellCompDirective
+	}{
+		{
+			name: "some cloud services",
+			args: args{
+				toComplete: "",
+			},
+			want: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
+				return assert.NotNil(tt, i1)
+			},
+			want1: cobra.ShellCompDirectiveNoFileComp,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, got1 := cli.ValidArgsGetCloudServices(tt.args.in0, tt.args.args, tt.args.toComplete)
+
+			if tt.want != nil {
+				tt.want(t, got)
+			}
+
+			if !reflect.DeepEqual(got1, tt.want1) {
+				t.Errorf("TestValidArgsGetCloudServices() got1 = %v, want %v", got1, tt.want1)
+			}
+		})
+	}
 }
diff --git a/cmd/engine/engine_test.go b/cmd/engine/engine_test.go
index 003624fa..ff6ca455 100644
--- a/cmd/engine/engine_test.go
+++ b/cmd/engine/engine_test.go
@@ -1,14 +1,24 @@
 package main
 
 import (
+	"os"
 	"testing"
 
+	"clouditor.io/clouditor/internal/testutil/clitest"
 	"clouditor.io/clouditor/rest"
+	service_discovery "clouditor.io/clouditor/service/discovery"
+
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
 )
 
+func TestMain(m *testing.M) {
+	clitest.AutoChdir()
+
+	os.Exit(m.Run())
+}
+
 func Test_doCmd(t *testing.T) {
 	type args struct {
 		in0 *cobra.Command
@@ -18,15 +28,25 @@ func Test_doCmd(t *testing.T) {
 		name      string
 		prepViper func()
 		args      args
+		want      assert.ValueAssertionFunc
 		wantErr   bool
 	}{
 		{
 			name: "Launch with --db-in-memory",
 			prepViper: func() {
 				viper.Set(DBInMemoryFlag, true)
+				viper.Set(APIStartEmbeddedOAuth2ServerFlag, true)
 				viper.Set(APIHTTPPortFlag, 0)
 				viper.Set(APIgRPCPortFlag, 0)
 			},
+			want: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
+				discoveryService := i1.(*service_discovery.Service)
+				if !assert.NotNil(t, discoveryService) {
+					return false
+				}
+
+				return assert.NotNil(t, discoveryService.Authorizer())
+			},
 		},
 		{
 			name: "Launch with invalid postgres port",
@@ -57,11 +77,14 @@ func Test_doCmd(t *testing.T) {
 
 			if success {
 				assert.NotNil(t, server)
-				assert.NotNil(t, authService)
 				assert.NotNil(t, discoveryService)
 				assert.NotNil(t, assessmentService)
 				assert.NotNil(t, evidenceStoreService)
 			}
+
+			if tt.want != nil {
+				tt.want(t, discoveryService)
+			}
 		})
 	}
 }
diff --git a/internal/auth/key_loader_test.go b/internal/auth/key_loader_test.go
new file mode 100644
index 00000000..eb1b2f24
--- /dev/null
+++ b/internal/auth/key_loader_test.go
@@ -0,0 +1,195 @@
+// Copyright 2016-2022 Fraunhofer AISEC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//           $$\                           $$\ $$\   $$\
+//           $$ |                          $$ |\__|  $$ |
+//  $$$$$$$\ $$ | $$$$$$\  $$\   $$\  $$$$$$$ |$$\ $$$$$$\    $$$$$$\   $$$$$$\
+// $$  _____|$$ |$$  __$$\ $$ |  $$ |$$  __$$ |$$ |\_$$  _|  $$  __$$\ $$  __$$\
+// $$ /      $$ |$$ /  $$ |$$ |  $$ |$$ /  $$ |$$ |  $$ |    $$ /  $$ |$$ | \__|
+// $$ |      $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$\ $$ |  $$ |$$ |
+// \$$$$$$\  $$ |\$$$$$   |\$$$$$   |\$$$$$$  |$$ |  \$$$   |\$$$$$   |$$ |
+//  \_______|\__| \______/  \______/  \_______|\__|   \____/  \______/ \__|
+//
+// This file is part of Clouditor Community Edition.
+
+package auth
+
+import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"io/ioutil"
+	"os"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_keyLoader_recoverFromLoadApiKeyError(t *testing.T) {
+	var tmpFile, _ = ioutil.TempFile("", "api.key")
+	// Close it immediately , since we want to write to it
+	tmpFile.Close()
+
+	defer func() {
+		os.Remove(tmpFile.Name())
+	}()
+
+	type fields struct {
+		saveOnCreate bool
+		path         string
+		password     string
+	}
+	type args struct {
+		err         error
+		defaultPath bool
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		wantKey assert.ValueAssertionFunc
+	}{
+		{
+			name: "Could not load key from custom path",
+			fields: fields{
+				saveOnCreate: false,
+				path:         "doesnotexist",
+				password:     "test",
+			},
+			args: args{
+				err:         os.ErrNotExist,
+				defaultPath: false,
+			},
+			wantKey: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
+				// A temporary key should be created
+				return assert.NotNil(tt, i1.(*ecdsa.PrivateKey))
+			},
+		},
+		{
+			name: "Could not load key from default path and save it",
+			fields: fields{
+				saveOnCreate: true,
+				path:         tmpFile.Name(),
+				password:     "test",
+			},
+			args: args{
+				err:         os.ErrNotExist,
+				defaultPath: true,
+			},
+			wantKey: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
+				// A temporary key should be created
+				if !assert.NotNil(tt, i1.(*ecdsa.PrivateKey)) {
+					return false
+				}
+
+				f, err := os.OpenFile(tmpFile.Name(), os.O_RDONLY, 0600)
+				if !assert.ErrorIs(tt, err, nil) {
+					return false
+				}
+
+				// Our tmp file should also contain something now
+				data, err := ioutil.ReadAll(f)
+				if !assert.ErrorIs(tt, err, nil) {
+					return false
+				}
+
+				return assert.True(tt, len(data) > 0)
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s := &keyLoader{
+				path:         tt.fields.path,
+				password:     tt.fields.password,
+				saveOnCreate: tt.fields.saveOnCreate,
+			}
+			gotKey := s.recoverFromLoadApiKeyError(tt.args.err, tt.args.defaultPath)
+
+			if tt.wantKey != nil {
+				tt.wantKey(t, gotKey, tt.args.err, tt.args.defaultPath)
+			}
+		})
+	}
+}
+
+func TestService_loadKeyFromFile(t *testing.T) {
+	// Prepare a tmp file that contains a new temporary private key
+	var tmpFile, _ = ioutil.TempFile("", "api.key")
+	tmpFile.Close()
+
+	// Create a new temporary key
+	tmpKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+
+	defer func() {
+		os.Remove(tmpFile.Name())
+	}()
+
+	// Save a key to it
+	err := saveKeyToFile(tmpKey, tmpFile.Name(), "tmp")
+	assert.NoError(t, err)
+
+	type args struct {
+		path     string
+		password []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantKey *ecdsa.PrivateKey
+		wantErr bool
+	}{
+		{
+			name: "Load existing key",
+			args: args{
+				path:     tmpFile.Name(),
+				password: []byte("tmp"),
+			},
+			wantKey: tmpKey,
+			wantErr: false,
+		},
+		{
+			name: "Load existing key with wrong password",
+			args: args{
+				path:     tmpFile.Name(),
+				password: []byte("notpassword"),
+			},
+			wantKey: nil,
+			wantErr: true,
+		},
+		{
+			name: "Load not existing key",
+			args: args{
+				path:     "notexists",
+				password: []byte("tmp"),
+			},
+			wantKey: nil,
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotKey, err := loadKeyFromFile(tt.args.path, tt.args.password)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Service.loadKeyFromFile() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(gotKey, tt.wantKey) {
+				t.Errorf("Service.loadKeyFromFile() = %v, want %v", gotKey, tt.wantKey)
+			}
+		})
+	}
+}
diff --git a/service/auth/pem.go b/internal/auth/pem.go
similarity index 100%
rename from service/auth/pem.go
rename to internal/auth/pem.go
diff --git a/service/auth/pem_test.go b/internal/auth/pem_test.go
similarity index 100%
rename from service/auth/pem_test.go
rename to internal/auth/pem_test.go
diff --git a/internal/testutil/auth.go b/internal/testutil/auth.go
new file mode 100644
index 00000000..d98b9159
--- /dev/null
+++ b/internal/testutil/auth.go
@@ -0,0 +1,63 @@
+package testutil
+
+import (
+	"fmt"
+	"net"
+
+	oauth2 "github.com/oxisto/oauth2go"
+	"github.com/oxisto/oauth2go/login"
+	"golang.org/x/oauth2/clientcredentials"
+)
+
+const (
+	TestAuthUser     = "clouditor"
+	TestAuthPassword = "clouditor"
+
+	TestAuthClientID     = "clouditor"
+	TestAuthClientSecret = "clouditor"
+)
+
+// StartAuthenticationServer starts an authentication server on a random port with
+// users and clients specified in the TestAuthUser and TestAuthClientID constants.
+func StartAuthenticationServer() (srv *oauth2.AuthorizationServer, port int, err error) {
+	var nl net.Listener
+
+	srv = oauth2.NewServer(":0",
+		oauth2.WithClient("cli", "", "http://localhost:10000/callback"),
+		login.WithLoginPage(login.WithUser(TestAuthUser, TestAuthPassword)),
+	)
+
+	// create a new socket for gRPC communication
+	nl, err = net.Listen("tcp", srv.Addr)
+	if err != nil {
+		return nil, 0, fmt.Errorf("could not listen: %w", err)
+	}
+
+	go func() {
+		_ = srv.Serve(nl)
+	}()
+
+	port = nl.Addr().(*net.TCPAddr).Port
+
+	return srv, port, nil
+}
+
+func JWKSURL(port int) string {
+	return fmt.Sprintf("http://localhost:%d/.well-known/jwks.json", port)
+}
+
+func TokenURL(port int) string {
+	return fmt.Sprintf("http://localhost:%d/token", port)
+}
+
+func AuthURL(port int) string {
+	return fmt.Sprintf("http://localhost:%d/authorize", port)
+}
+
+func AuthClientConfig(port int) *clientcredentials.Config {
+	return &clientcredentials.Config{
+		ClientID:     TestAuthClientID,
+		ClientSecret: TestAuthClientSecret,
+		TokenURL:     TokenURL(port),
+	}
+}
diff --git a/internal/testutil/clitest/cli.go b/internal/testutil/clitest/cli.go
new file mode 100644
index 00000000..5a6df61a
--- /dev/null
+++ b/internal/testutil/clitest/cli.go
@@ -0,0 +1,134 @@
+package clitest
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"os"
+	"testing"
+
+	"clouditor.io/clouditor/api"
+	"clouditor.io/clouditor/cli"
+	"clouditor.io/clouditor/internal/testutil"
+	"clouditor.io/clouditor/service"
+	oauth2 "github.com/oxisto/oauth2go"
+	"github.com/spf13/viper"
+	"google.golang.org/grpc"
+)
+
+// PrepareSession prepares a session for unit tests. It creates a temporary folder to save
+// the session credentials in and does a login to the specified authorization server using
+// test credentials. It is the responsibility of the caller to cleanup the temporary directory.
+//
+// This function will use asserts and fail/panic if errors occurs.
+func PrepareSession(authPort int, authSrv *oauth2.AuthorizationServer, grpcURL string) (dir string, err error) {
+	var (
+		token   *oauth2.Token
+		session *cli.Session
+	)
+
+	// Create a temporary folder
+	dir, err = ioutil.TempDir(os.TempDir(), ".clouditor")
+	if err != nil {
+		return "", err
+	}
+
+	viper.Set("auth-server", fmt.Sprintf("http://localhost:%d", authPort))
+	viper.Set(cli.SessionFolderFlag, dir)
+
+	// Simulate a login by directly granting a token
+	token, err = authSrv.GenerateToken(testutil.TestAuthClientID, 0, 0)
+	if err != nil {
+		return "", err
+	}
+
+	// TODO(oxisto): This is slightly duplicated code from the Login command. Extract it into the session struct
+	session, err = cli.NewSession(grpcURL, &oauth2.Config{
+		ClientID: testutil.TestAuthClientID,
+		Endpoint: oauth2.Endpoint{
+			AuthURL:  testutil.AuthURL(authPort),
+			TokenURL: testutil.TokenURL(authPort),
+		},
+	}, token)
+	if err != nil {
+		return "", err
+	}
+
+	session.SetAuthorizer(api.NewOAuthAuthorizerFromConfig(
+		session.Config,
+		token,
+	))
+
+	err = session.Save()
+	if err != nil {
+		return "", err
+	}
+
+	return dir, nil
+}
+
+// RunCLITest can be used in a TestMain function for CLI tests. It takes care of launching
+// an authorization server as well as a gRPC server with the selected services supplied as options.
+// It also automatically issues a login command to the auth service.
+//
+// Since this function is primarily used in a TestMain and no testing.T object is available at this
+// point, this function WILL panic on errors.
+func RunCLITest(m *testing.M, opts ...service.StartGRPCServerOption) (code int) {
+	var (
+		err      error
+		tmpDir   string
+		auth     *oauth2.AuthorizationServer
+		authPort int
+		grpcPort int
+		sock     net.Listener
+		server   *grpc.Server
+	)
+
+	auth, authPort, err = testutil.StartAuthenticationServer()
+	if err != nil {
+		panic(err)
+	}
+
+	sock, server, err = service.StartGRPCServer(testutil.JWKSURL(authPort), opts...)
+	if err != nil {
+		panic(err)
+	}
+
+	grpcPort = sock.Addr().(*net.TCPAddr).Port
+
+	tmpDir, err = PrepareSession(authPort, auth, fmt.Sprintf("localhost:%d", grpcPort))
+	if err != nil {
+		panic(err)
+	}
+
+	code = m.Run()
+
+	sock.Close()
+	server.Stop()
+
+	// Remove temporary session directory
+	os.RemoveAll(tmpDir)
+
+	return code
+}
+
+// AutoChdir automatically guesses if we need to change the current working directory
+// so that we can find the policies folder
+func AutoChdir() {
+	var (
+		err error
+	)
+
+	// Check, if we can find the policies folder
+	_, err = os.Stat("policies")
+	if errors.Is(err, os.ErrNotExist) {
+		// Try again one level deeper
+		err = os.Chdir("..")
+		if err != nil {
+			panic(err)
+		}
+
+		AutoChdir()
+	}
+}
diff --git a/policies/policies_test.go b/policies/policies_test.go
index b0cb4052..ca294a23 100644
--- a/policies/policies_test.go
+++ b/policies/policies_test.go
@@ -31,6 +31,7 @@ import (
 	"os"
 	"testing"
 
+	"clouditor.io/clouditor/internal/testutil/clitest"
 	"clouditor.io/clouditor/voc"
 	"google.golang.org/protobuf/encoding/protojson"
 
@@ -51,11 +52,7 @@ const (
 )
 
 func TestMain(m *testing.M) {
-	// make sure, that we are in the clouditor root folder to find the policies
-	err := os.Chdir("../")
-	if err != nil {
-		panic(err)
-	}
+	clitest.AutoChdir()
 
 	os.Exit(m.Run())
 }
diff --git a/rest/rest_test.go b/rest/rest_test.go
index c45424aa..650e3f11 100644
--- a/rest/rest_test.go
+++ b/rest/rest_test.go
@@ -26,7 +26,6 @@
 package rest
 
 import (
-	"bytes"
 	"context"
 	"errors"
 	"fmt"
@@ -39,7 +38,10 @@ import (
 	"testing"
 	"time"
 
+	"clouditor.io/clouditor/internal/testutil"
+	"clouditor.io/clouditor/internal/testutil/clitest"
 	"clouditor.io/clouditor/service"
+	service_orchestrator "clouditor.io/clouditor/service/orchestrator"
 
 	"github.com/stretchr/testify/assert"
 	"google.golang.org/grpc"
@@ -55,13 +57,21 @@ var (
 
 func TestMain(m *testing.M) {
 	var (
-		err    error
-		server *grpc.Server
-		sock   net.Listener
+		err      error
+		server   *grpc.Server
+		sock     net.Listener
+		authPort int
 	)
 
-	// Start at least an authentication server, so that we have something to forward
-	sock, server, _, err = service.StartDedicatedAuthServer(":0")
+	clitest.AutoChdir()
+
+	_, authPort, err = testutil.StartAuthenticationServer()
+	if err != nil {
+		panic(err)
+	}
+
+	// Start at least an orchestrator service, so that we have something to forward
+	sock, server, err = service.StartGRPCServer(testutil.JWKSURL(authPort), service.WithOrchestrator(service_orchestrator.NewService()))
 	if err != nil {
 		panic(err)
 	}
@@ -147,7 +157,7 @@ func TestREST(t *testing.T) {
 			args: args{
 				origin:    "clouditor.io",
 				method:    "POST",
-				url:       "v1/auth/login",
+				url:       "v1/orchestrator/cloud_services",
 				preflight: false,
 			},
 			statusCode: 401, // because we are not supplying an actual login request
@@ -162,7 +172,7 @@ func TestREST(t *testing.T) {
 			args: args{
 				origin:    "clouditor.com",
 				method:    "POST",
-				url:       "v1/auth/login",
+				url:       "v1/orchestrator/cloud_services",
 				preflight: true,
 			},
 			statusCode: 501,
@@ -175,7 +185,7 @@ func TestREST(t *testing.T) {
 			args: args{
 				origin:    "clouditor.com",
 				method:    "POST",
-				url:       "v1/auth/login",
+				url:       "v1/orchestrator/cloud_services",
 				preflight: false,
 			},
 			statusCode: 401, // because we are not supplying an actual login request
@@ -205,31 +215,6 @@ func TestREST(t *testing.T) {
 				return assert.Equal(tt, []byte("just a test"), content)
 			},
 		},
-		{
-			// Successful token responses are tested in auth_test.go
-			name: "Form request with invalid grant type",
-			args: args{
-				method:      "POST",
-				url:         "v1/auth/token",
-				body:        bytes.NewReader([]byte(`grant_type=authorization_code`)),
-				contentType: "application/x-www-form-urlencoded",
-				preflight:   false,
-			},
-			statusCode: 400,
-			wantResponse: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
-				resp, ok := i1.(*http.Response)
-				if !ok {
-					return assert.True(tt, ok)
-				}
-
-				content, err := ioutil.ReadAll(resp.Body)
-				if !assert.ErrorIs(tt, err, nil) {
-					return false
-				}
-
-				return assert.Equal(tt, []byte(`{"error":"unsupported_grant_type"}`), content)
-			},
-		},
 	}
 
 	for _, tt := range tests {
diff --git a/service/assessment/assessment_test.go b/service/assessment/assessment_test.go
index 0a888729..52e2fefb 100644
--- a/service/assessment/assessment_test.go
+++ b/service/assessment/assessment_test.go
@@ -39,6 +39,8 @@ import (
 
 	"clouditor.io/clouditor/api/assessment"
 	"clouditor.io/clouditor/api/evidence"
+	"clouditor.io/clouditor/internal/testutil"
+	"clouditor.io/clouditor/internal/testutil/clitest"
 	"clouditor.io/clouditor/voc"
 
 	"github.com/stretchr/testify/assert"
@@ -50,22 +52,19 @@ import (
 	"google.golang.org/protobuf/types/known/timestamppb"
 )
 
+var (
+	authPort int
+)
+
 func TestMain(m *testing.M) {
-	// make sure, that we are in the clouditor root folder to find the policies
-	err := os.Chdir("../../")
-	if err != nil {
-		panic(err)
-	}
+	clitest.AutoChdir()
 
-	server, authService, _, _ := startBufConnServer()
-	err = authService.CreateDefaultUser("clouditor", "clouditor")
-	if err != nil {
-		panic(err)
-	}
+	server, _, _ := startBufConnServer()
 
 	code := m.Run()
 
 	server.Stop()
+
 	os.Exit(code)
 }
 
@@ -877,12 +876,7 @@ func TestService_initEvidenceStoreStream(t *testing.T) {
 			fields: fields{
 				opts: []ServiceOption{
 					WithEvidenceStoreAddress("bufnet"),
-					WithInternalAuthorizer(
-						"bufnet",
-						"clouditor",
-						"clouditor",
-						grpc.WithContextDialer(bufConnDialer),
-					),
+					WithOAuth2Authorizer(testutil.AuthClientConfig(authPort)),
 				},
 			},
 			args: args{
@@ -894,19 +888,15 @@ func TestService_initEvidenceStoreStream(t *testing.T) {
 			fields: fields{
 				opts: []ServiceOption{
 					WithEvidenceStoreAddress("bufnet"),
-					WithInternalAuthorizer(
-						"bufnet",
-						"not_clouditor",
-						"clouditor",
-						grpc.WithContextDialer(bufConnDialer),
-					),
+					WithOAuth2Authorizer(testutil.AuthClientConfig(authPort)),
 				},
 			},
 			args: args{
 				[]grpc.DialOption{grpc.WithContextDialer(bufConnDialer)},
 			},
 			wantErr: func(tt assert.TestingT, err error, i ...interface{}) bool {
-				return assert.Equal(tt, err.Error(), "could not set up stream for storing evidences: rpc error: code = Unauthenticated desc = transport: per-RPC creds failed due to error: error while logging in: rpc error: code = Unauthenticated desc = login failed")
+				s, _ := status.FromError(errors.Unwrap(err))
+				return assert.Equal(t, codes.Unauthenticated, s.Code())
 			},
 		},
 	}
@@ -944,16 +934,8 @@ func TestService_initOrchestratorStoreStream(t *testing.T) {
 				},
 			},
 			wantErr: func(tt assert.TestingT, err error, i ...interface{}) bool {
-				// We are looking for a connection refused message
-				innerErr := errors.Unwrap(err)
-				s, _ := status.FromError(innerErr)
-
-				if s.Code() != codes.Unavailable {
-					tt.Errorf("Status should be codes.Unavailable: %v", s.Code())
-					return false
-				}
-
-				return true
+				s, _ := status.FromError(errors.Unwrap(err))
+				return assert.Equal(t, codes.Unavailable, s.Code())
 			},
 		},
 		{
@@ -961,12 +943,7 @@ func TestService_initOrchestratorStoreStream(t *testing.T) {
 			fields: fields{
 				opts: []ServiceOption{
 					WithOrchestratorAddress("bufnet"),
-					WithInternalAuthorizer(
-						"bufnet",
-						"clouditor",
-						"clouditor",
-						grpc.WithContextDialer(bufConnDialer),
-					),
+					WithOAuth2Authorizer(testutil.AuthClientConfig(authPort)),
 				},
 			},
 			args: args{
@@ -978,19 +955,15 @@ func TestService_initOrchestratorStoreStream(t *testing.T) {
 			fields: fields{
 				opts: []ServiceOption{
 					WithOrchestratorAddress("bufnet"),
-					WithInternalAuthorizer(
-						"bufnet",
-						"not_clouditor",
-						"clouditor",
-						grpc.WithContextDialer(bufConnDialer),
-					),
+					WithOAuth2Authorizer(testutil.AuthClientConfig(authPort)),
 				},
 			},
 			args: args{
 				[]grpc.DialOption{grpc.WithContextDialer(bufConnDialer)},
 			},
 			wantErr: func(tt assert.TestingT, err error, i ...interface{}) bool {
-				return assert.Equal(tt, err.Error(), "could not set up stream for storing assessment results: rpc error: code = Unauthenticated desc = transport: per-RPC creds failed due to error: error while logging in: rpc error: code = Unauthenticated desc = login failed")
+				s, _ := status.FromError(errors.Unwrap(err))
+				return assert.Equal(t, codes.Unauthenticated, s.Code())
 			},
 		},
 	}
diff --git a/service/assessment/bufconn_test.go b/service/assessment/bufconn_test.go
index 902c951e..643676af 100644
--- a/service/assessment/bufconn_test.go
+++ b/service/assessment/bufconn_test.go
@@ -29,10 +29,8 @@ import (
 	"context"
 	"net"
 
-	"clouditor.io/clouditor/api/auth"
 	"clouditor.io/clouditor/api/evidence"
 	"clouditor.io/clouditor/api/orchestrator"
-	service_auth "clouditor.io/clouditor/service/auth"
 	service_evidence "clouditor.io/clouditor/service/evidence"
 	service_orchestrator "clouditor.io/clouditor/service/orchestrator"
 	"google.golang.org/grpc"
@@ -54,15 +52,11 @@ func bufConnDialer(context.Context, string) (net.Conn, error) {
 // * Auth
 // * Orchestrator
 // * Evidence Store
-func startBufConnServer() (*grpc.Server, *service_auth.Service, *service_orchestrator.Service, *service_evidence.Service) {
+func startBufConnServer() (*grpc.Server, *service_orchestrator.Service, *service_evidence.Service) {
 	bufConnListener = bufconn.Listen(DefaultBufferSize)
 
 	server := grpc.NewServer()
 
-	// We do not want a persistent key storage here
-	authService := service_auth.NewService(service_auth.WithApiKeySaveOnCreate(false))
-	auth.RegisterAuthenticationServer(server, authService)
-
 	orchestratorService := service_orchestrator.NewService()
 	orchestrator.RegisterOrchestratorServer(server, orchestratorService)
 
@@ -75,5 +69,5 @@ func startBufConnServer() (*grpc.Server, *service_auth.Service, *service_orchest
 		}
 	}()
 
-	return server, authService, orchestratorService, evidenceService
+	return server, orchestratorService, evidenceService
 }
diff --git a/service/auth/auth_test.go b/service/auth/auth_test.go
deleted file mode 100644
index 7e46b928..00000000
--- a/service/auth/auth_test.go
+++ /dev/null
@@ -1,564 +0,0 @@
-// Copyright 2016-2022 Fraunhofer AISEC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-//           $$\                           $$\ $$\   $$\
-//           $$ |                          $$ |\__|  $$ |
-//  $$$$$$$\ $$ | $$$$$$\  $$\   $$\  $$$$$$$ |$$\ $$$$$$\    $$$$$$\   $$$$$$\
-// $$  _____|$$ |$$  __$$\ $$ |  $$ |$$  __$$ |$$ |\_$$  _|  $$  __$$\ $$  __$$\
-// $$ /      $$ |$$ /  $$ |$$ |  $$ |$$ /  $$ |$$ |  $$ |    $$ /  $$ |$$ | \__|
-// $$ |      $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$\ $$ |  $$ |$$ |
-// \$$$$$$\  $$ |\$$$$$   |\$$$$$   |\$$$$$$  |$$ |  \$$$   |\$$$$$   |$$ |
-//  \_______|\__| \______/  \______/  \_______|\__|   \____/  \______/ \__|
-//
-// This file is part of Clouditor Community Edition.
-
-package auth
-
-import (
-	"context"
-	"crypto/ecdsa"
-	"crypto/elliptic"
-	"crypto/rand"
-	"errors"
-	"io/ioutil"
-	"math/big"
-	"os"
-	"reflect"
-	"testing"
-
-	"clouditor.io/clouditor/api/auth"
-	"clouditor.io/clouditor/persistence/inmemory"
-	"github.com/stretchr/testify/assert"
-	"google.golang.org/protobuf/proto"
-)
-
-func TestService_ListPublicKeys(t *testing.T) {
-	type fields struct {
-		apiKey *ecdsa.PrivateKey
-	}
-	type args struct {
-		in0 context.Context
-		in1 *auth.ListPublicKeysRequest
-	}
-	tests := []struct {
-		name         string
-		fields       fields
-		args         args
-		wantResponse *auth.ListPublicResponse
-		wantErr      bool
-	}{
-		{
-			name: "List single public key",
-			fields: fields{
-				apiKey: &ecdsa.PrivateKey{
-					PublicKey: ecdsa.PublicKey{
-						Curve: elliptic.P256(),
-						X:     big.NewInt(1),
-						Y:     big.NewInt(2),
-					},
-				},
-			},
-			args: args{
-				in0: context.TODO(),
-				in1: &auth.ListPublicKeysRequest{},
-			},
-			wantResponse: &auth.ListPublicResponse{
-				Keys: []*auth.JsonWebKey{
-					{
-						Kid: "1",
-						Kty: "EC",
-						Crv: "P-256",
-						X:   "AQ",
-						Y:   "Ag",
-					},
-				},
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			s := Service{
-				apiKey: tt.fields.apiKey,
-			}
-			gotResponse, err := s.ListPublicKeys(tt.args.in0, tt.args.in1)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("Service.ListPublicKeys() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !proto.Equal(gotResponse, tt.wantResponse) {
-				t.Errorf("Service.ListPublicKeys() = %v, want %v", gotResponse, tt.wantResponse)
-			}
-		})
-	}
-}
-
-func TestService_Token(t *testing.T) {
-	// Create a new temporary key
-	tmpKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-
-	// Create a refresh token
-	refreshToken, _ := issueRefreshToken(tmpKey, "clouditor")
-
-	type fields struct {
-		apiKey *ecdsa.PrivateKey
-	}
-	type args struct {
-		ctx context.Context
-		req *auth.TokenRequest
-	}
-	tests := []struct {
-		name         string
-		fields       fields
-		args         args
-		wantResponse assert.ValueAssertionFunc
-		wantErr      bool
-	}{
-		{
-			name:    "Missing requirement parameter",
-			wantErr: true,
-		},
-		{
-			name: "Valid refresh_token",
-			fields: fields{
-				apiKey: tmpKey,
-			},
-			args: args{
-				req: &auth.TokenRequest{GrantType: "refresh_token", RefreshToken: refreshToken},
-			},
-			wantResponse: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
-				resp, ok := i1.(*auth.TokenResponse)
-				if !assert.True(tt, ok) {
-					return false
-				}
-
-				return assert.NotNil(tt, resp.AccessToken)
-			},
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			s := &Service{
-				apiKey: tt.fields.apiKey,
-			}
-
-			var err error
-
-			s.storage, err = inmemory.NewStorage()
-			assert.NoError(t, err)
-
-			err = s.CreateDefaultUser("clouditor", "clouditor")
-			assert.NoError(t, err)
-
-			gotResponse, err := s.Token(tt.args.ctx, tt.args.req)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("Service.Token() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-
-			if tt.wantResponse != nil {
-				tt.wantResponse(t, gotResponse, tt.args.ctx, tt.args.req)
-			}
-		})
-	}
-}
-
-func TestService_recoverFromLoadApiKeyError(t *testing.T) {
-	var tmpFile, _ = ioutil.TempFile("", "api.key")
-	// Close it immediately , since we want to write to it
-	tmpFile.Close()
-
-	defer func() {
-		os.Remove(tmpFile.Name())
-	}()
-
-	type fields struct {
-		config struct {
-			keySaveOnCreate bool
-			keyPath         string
-			keyPassword     string
-		}
-		apiKey *ecdsa.PrivateKey
-	}
-	type args struct {
-		err         error
-		defaultPath bool
-	}
-	tests := []struct {
-		name        string
-		fields      fields
-		args        args
-		wantService assert.ValueAssertionFunc
-	}{
-		{
-			name: "Could not load key from custom path",
-			fields: fields{
-				config: struct {
-					keySaveOnCreate bool
-					keyPath         string
-					keyPassword     string
-				}{
-					keySaveOnCreate: false,
-					keyPath:         "doesnotexist",
-					keyPassword:     "test",
-				},
-			},
-			args: args{
-				err:         os.ErrNotExist,
-				defaultPath: false,
-			},
-			wantService: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
-				// A temporary key should be created
-				return assert.NotNil(tt, i1.(*Service).apiKey)
-			},
-		},
-		{
-			name: "Could not load key from default path and save it",
-			fields: fields{
-				config: struct {
-					keySaveOnCreate bool
-					keyPath         string
-					keyPassword     string
-				}{
-					keySaveOnCreate: true,
-					keyPath:         tmpFile.Name(),
-					keyPassword:     "test",
-				},
-			},
-			args: args{
-				err:         os.ErrNotExist,
-				defaultPath: true,
-			},
-			wantService: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
-				// A temporary key should be created
-				if !assert.NotNil(tt, i1.(*Service).apiKey) {
-					return false
-				}
-
-				f, err := os.OpenFile(tmpFile.Name(), os.O_RDONLY, 0600)
-				if !assert.ErrorIs(tt, err, nil) {
-					return false
-				}
-
-				// Our tmp file should also contain something now
-				data, err := ioutil.ReadAll(f)
-				if !assert.ErrorIs(tt, err, nil) {
-					return false
-				}
-
-				return assert.True(tt, len(data) > 0)
-			},
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			s := &Service{
-				config: tt.fields.config,
-				apiKey: tt.fields.apiKey,
-			}
-			s.recoverFromLoadApiKeyError(tt.args.err, tt.args.defaultPath)
-
-			if tt.wantService != nil {
-				tt.wantService(t, s, tt.args.err, tt.args.defaultPath)
-			}
-		})
-	}
-}
-
-func TestService_loadApiKey(t *testing.T) {
-	// Prepare a tmp file that contains a new temporary private key
-	var tmpFile, _ = ioutil.TempFile("", "api.key")
-	tmpFile.Close()
-
-	// Create a new temporary key
-	tmpKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-
-	defer func() {
-		os.Remove(tmpFile.Name())
-	}()
-
-	// Save a key to it
-	err := saveApiKey(tmpKey, tmpFile.Name(), "tmp")
-	assert.NoError(t, err)
-
-	type args struct {
-		path     string
-		password []byte
-	}
-	tests := []struct {
-		name    string
-		args    args
-		wantKey *ecdsa.PrivateKey
-		wantErr bool
-	}{
-		{
-			name: "Load existing key",
-			args: args{
-				path:     tmpFile.Name(),
-				password: []byte("tmp"),
-			},
-			wantKey: tmpKey,
-			wantErr: false,
-		},
-		{
-			name: "Load existing key with wrong password",
-			args: args{
-				path:     tmpFile.Name(),
-				password: []byte("notpassword"),
-			},
-			wantKey: nil,
-			wantErr: true,
-		},
-		{
-			name: "Load not existing key",
-			args: args{
-				path:     "notexists",
-				password: []byte("tmp"),
-			},
-			wantKey: nil,
-			wantErr: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			gotKey, err := loadApiKey(tt.args.path, tt.args.password)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("Service.loadApiKey() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(gotKey, tt.wantKey) {
-				t.Errorf("Service.loadApiKey() = %v, want %v", gotKey, tt.wantKey)
-			}
-		})
-	}
-}
-
-func TestNewService(t *testing.T) {
-	var myStorage, err = inmemory.NewStorage()
-	assert.NoError(t, err)
-
-	type args struct {
-		opts []ServiceOption
-	}
-	tests := []struct {
-		name string
-		args args
-		want assert.ValueAssertionFunc
-	}{
-		{
-			name: "New service with database",
-			args: args{
-				opts: []ServiceOption{WithStorage(myStorage)},
-			},
-			want: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
-				service, ok := i1.(*Service)
-				if !assert.True(tt, ok) {
-					return false
-				}
-
-				return assert.Equal(tt, myStorage, service.storage)
-			},
-		},
-		{
-			name: "New service with Api Key Path",
-			args: args{
-				opts: []ServiceOption{WithApiKeyPath("SomePath")},
-			},
-			want: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
-				service, ok := i1.(*Service)
-				if !assert.True(tt, ok) {
-					return false
-				}
-
-				return assert.Equal(tt, "SomePath", service.config.keyPath)
-			},
-		},
-		{
-			name: "New service with Api Key Save on Create",
-			args: args{
-				opts: []ServiceOption{WithApiKeySaveOnCreate(true)},
-			},
-			want: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
-				service, ok := i1.(*Service)
-				if !assert.True(tt, ok) {
-					return false
-				}
-
-				return assert.Equal(tt, true, service.config.keySaveOnCreate)
-			},
-		},
-		{
-			name: "New service with Api Key Save on Create",
-			args: args{
-				opts: []ServiceOption{WithApiKeySaveOnCreate(false)},
-			},
-			want: func(tt assert.TestingT, i1 interface{}, i2 ...interface{}) bool {
-				service, ok := i1.(*Service)
-				if !assert.True(tt, ok) {
-					return false
-				}
-
-				return assert.Equal(tt, false, service.config.keySaveOnCreate)
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got := NewService(tt.args.opts...)
-
-			if tt.want != nil {
-				tt.want(t, got, tt.args.opts)
-			}
-		})
-	}
-}
-
-func TestService_CreateDefaultUser(t *testing.T) {
-	// Instantiate a mock storage per error
-	var mStorageCount = mockStorage{countError: errors.New("mock storage db error: count")}
-	var mStorageCreate = mockStorage{createError: errors.New("mock storage db error: create")}
-
-	type fields struct {
-		config struct {
-			keySaveOnCreate bool
-			keyPath         string
-			keyPassword     string
-		}
-		apiKey      *ecdsa.PrivateKey
-		mockStorage *mockStorage
-	}
-	type args struct {
-		username string
-		password string
-	}
-	tests := []struct {
-		name         string
-		fields       fields
-		args         args
-		preFunctions []func(s *Service)
-		wantErr      assert.ErrorAssertionFunc
-	}{
-		{
-			name: "Storage count error",
-			args: args{
-				username: "",
-				password: "",
-			},
-			fields: fields{
-				mockStorage: &mStorageCount,
-			},
-			wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
-				return assert.ErrorIs(t, err, mStorageCount.countError)
-			},
-		},
-		{
-			name: "Storage not empty - dont create default user",
-			args: args{
-				username: "",
-				password: "",
-			},
-			preFunctions: []func(s *Service){
-				func(s *Service) {
-					err := s.storage.Create(&auth.User{Username: "SomeName", Password: "SomePassword"})
-					assert.NoError(t, err)
-				},
-			},
-			fields: fields{
-				mockStorage: nil,
-			},
-			wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
-				return assert.ErrorIs(t, err, nil)
-			},
-		},
-		{
-			name: "Storage not empty but storage create error",
-			args: args{
-				username: "SomeName",
-				password: "SomePassword",
-			},
-			fields: fields{
-				mockStorage: &mStorageCreate,
-			},
-			wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
-				return assert.ErrorIs(t, err, mStorageCreate.createError)
-			},
-		},
-		{
-			name: "Storage empty - create default user",
-			args: args{
-				username: "SomeName",
-				password: "SomePassword",
-			},
-			fields: fields{
-				mockStorage: nil,
-			},
-			wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
-				assert.ErrorIs(t, err, nil)
-				var gotUser auth.User
-				err = i[0].(*Service).storage.Get(&gotUser, "username = ?", "SomeName")
-				return assert.ErrorIs(t, err, nil)
-			},
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			s := &Service{
-				config: tt.fields.config,
-				apiKey: tt.fields.apiKey,
-			}
-			if tt.fields.mockStorage != nil {
-				s.storage = tt.fields.mockStorage
-			} else {
-				s.storage, _ = inmemory.NewStorage()
-			}
-			for _, f := range tt.preFunctions {
-				f(s)
-			}
-			tt.wantErr(t, s.CreateDefaultUser(tt.args.username, tt.args.password), s)
-		})
-	}
-}
-
-// mockStorage is a mocked persistence.Storage implementation that returns errors at the specified
-// operations.
-//
-// TODO(lebogg): Extract this struct into our new internal/testutils package
-type mockStorage struct {
-	createError error
-	saveError   error
-	updateError error
-	getError    error
-	listError   error
-	countError  error
-	deleteError error
-}
-
-func (m mockStorage) Create(interface{}) error { return m.createError }
-
-func (m mockStorage) Save(interface{}, ...interface{}) error { return m.saveError }
-
-func (m mockStorage) Update(interface{}, interface{}, ...interface{}) error {
-	return m.updateError
-}
-
-func (m mockStorage) Get(interface{}, ...interface{}) error { return m.getError }
-
-func (m mockStorage) List(interface{}, ...interface{}) error { return m.listError }
-
-func (m mockStorage) Count(interface{}, ...interface{}) (int64, error) {
-	return 0, m.countError
-}
-
-func (m mockStorage) Delete(interface{}, ...interface{}) error { return m.deleteError }
diff --git a/service/auth_test.go b/service/auth_middleware_test.go
similarity index 51%
rename from service/auth_test.go
rename to service/auth_middleware_test.go
index 3e7f37f7..608e4049 100644
--- a/service/auth_test.go
+++ b/service/auth_middleware_test.go
@@ -27,54 +27,19 @@ package service
 
 import (
 	"context"
-	"errors"
 	"fmt"
-	"net"
-	"net/http"
-	"os"
-	"reflect"
 	"testing"
-	"time"
 
-	"clouditor.io/clouditor/api/auth"
-	"clouditor.io/clouditor/rest"
-	service_auth "clouditor.io/clouditor/service/auth"
+	"clouditor.io/clouditor/internal/testutil"
+
 	"github.com/golang-jwt/jwt/v4"
+	oauth2 "github.com/oxisto/oauth2go"
 	"github.com/stretchr/testify/assert"
-	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/metadata"
 	"google.golang.org/grpc/status"
 )
 
-var (
-	grpcPort    int
-	authService *service_auth.Service
-)
-
-func TestMain(m *testing.M) {
-	var (
-		err    error
-		server *grpc.Server
-		sock   net.Listener
-	)
-
-	// Start at least an authentication server, so that we have something to forward
-	sock, server, authService, err = StartDedicatedAuthServer(":0")
-	if err != nil {
-		panic(err)
-	}
-
-	grpcPort = sock.Addr().(*net.TCPAddr).Port
-
-	exit := m.Run()
-
-	sock.Close()
-	server.Stop()
-
-	os.Exit(exit)
-}
-
 func ValidClaimAssertion(tt assert.TestingT, i1 interface{}, _ ...interface{}) bool {
 	ctx, ok := i1.(context.Context)
 	if !ok {
@@ -97,34 +62,21 @@ func ValidClaimAssertion(tt assert.TestingT, i1 interface{}, _ ...interface{}) b
 }
 
 func TestAuthConfig_AuthFunc(t *testing.T) {
-	// We need to start a REST server for JWKS (using our auth server)
-	go func() {
-		err := rest.RunServer(
-			context.Background(),
-			grpcPort,
-			0,
-		)
-		if err != nil && !errors.Is(err, http.ErrServerClosed) {
-			panic(err)
-		}
-	}()
-	defer rest.StopServer(context.Background())
-
-	// Wait until server is ready to serve
-	select {
-	case <-rest.GetReadyChannel():
-		break
-	case <-time.After(10 * time.Second):
-		log.Println("Timeout while waiting for REST API")
-	}
+	var (
+		authSrv *oauth2.AuthorizationServer
+		err     error
+		port    int
+	)
 
-	port, err := rest.GetServerPort()
+	// We need to start a REST server for JWKS (using our auth server)
+	authSrv, port, err = testutil.StartAuthenticationServer()
 	assert.NoError(t, err)
+	defer authSrv.Close()
 
 	// Some pre-work to retrieve a valid token
-	loginResponse, err := authService.Login(context.TODO(), &auth.LoginRequest{Username: "clouditor", Password: "clouditor"})
+	token, err := authSrv.GenerateToken(testutil.TestAuthClientID, 0, 0)
 	assert.NoError(t, err)
-	assert.NotNil(t, loginResponse)
+	assert.NotNil(t, token)
 
 	type configureArgs struct {
 		opts []AuthOption
@@ -143,17 +95,17 @@ func TestAuthConfig_AuthFunc(t *testing.T) {
 		{
 			name: "Request with valid bearer token using JWKS",
 			configureArgs: configureArgs{
-				opts: []AuthOption{WithJWKSURL(fmt.Sprintf("http://localhost:%d/.well-known/jwks.json", port))},
+				opts: []AuthOption{WithJWKSURL(testutil.JWKSURL(port))},
 			},
 			args: args{
-				ctx: metadata.NewIncomingContext(context.TODO(), metadata.MD{"Authorization": []string{fmt.Sprintf("bearer %s", loginResponse.AccessToken)}}),
+				ctx: metadata.NewIncomingContext(context.TODO(), metadata.MD{"Authorization": []string{fmt.Sprintf("bearer %s", token.AccessToken)}}),
 			},
 			wantCtx: ValidClaimAssertion,
 		},
 		{
 			name: "Request with invalid bearer token using JWKS",
 			configureArgs: configureArgs{
-				opts: []AuthOption{WithJWKSURL(fmt.Sprintf("http://localhost:%d/.well-known/jwks.json", port))},
+				opts: []AuthOption{WithJWKSURL(testutil.JWKSURL(port))},
 			},
 			args: args{
 				ctx: metadata.NewIncomingContext(context.TODO(), metadata.MD{"Authorization": []string{"bearer not_really"}}),
@@ -165,7 +117,7 @@ func TestAuthConfig_AuthFunc(t *testing.T) {
 		{
 			name: "Request without bearer token using JWKS",
 			configureArgs: configureArgs{
-				opts: []AuthOption{WithJWKSURL(fmt.Sprintf("http://localhost:%d/.well-known/jwks.json", port))},
+				opts: []AuthOption{WithJWKSURL(testutil.JWKSURL(port))},
 			},
 			args: args{
 				ctx: context.TODO(),
@@ -177,17 +129,17 @@ func TestAuthConfig_AuthFunc(t *testing.T) {
 		{
 			name: "Request with valid bearer token using a public key",
 			configureArgs: configureArgs{
-				opts: []AuthOption{WithPublicKey(authService.GetPublicKey())},
+				opts: []AuthOption{WithPublicKey(authSrv.PublicKeys()[0])},
 			},
 			args: args{
-				ctx: metadata.NewIncomingContext(context.TODO(), metadata.MD{"Authorization": []string{fmt.Sprintf("bearer %s", loginResponse.AccessToken)}}),
+				ctx: metadata.NewIncomingContext(context.TODO(), metadata.MD{"Authorization": []string{fmt.Sprintf("bearer %s", token.AccessToken)}}),
 			},
 			wantCtx: ValidClaimAssertion,
 		},
 		{
 			name: "Request without bearer token using a public key",
 			configureArgs: configureArgs{
-				opts: []AuthOption{WithPublicKey(authService.GetPublicKey())},
+				opts: []AuthOption{WithPublicKey(authSrv.PublicKeys()[0])},
 			},
 			args: args{
 				ctx: context.TODO(),
@@ -197,6 +149,7 @@ func TestAuthConfig_AuthFunc(t *testing.T) {
 			},
 		},
 	}
+
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			config := ConfigureAuth(tt.configureArgs.opts...)
@@ -216,79 +169,3 @@ func TestAuthConfig_AuthFunc(t *testing.T) {
 		})
 	}
 }
-
-func TestStartDedicatedAuthServer(t *testing.T) {
-	type args struct {
-		address string
-		opts    []service_auth.ServiceOption
-	}
-	tests := []struct {
-		name            string
-		args            args
-		wantSock        net.Listener
-		wantServer      *grpc.Server
-		wantAuthService *service_auth.Service
-		wantErr         bool
-	}{
-		{
-			name: "Could not create default user",
-			args: args{
-				opts: []service_auth.ServiceOption{service_auth.WithStorage(mockStorage{
-					createError: errors.New("could not create"),
-				})},
-			},
-			wantErr: true,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			gotSock, gotServer, gotAuthService, err := StartDedicatedAuthServer(tt.args.address, tt.args.opts...)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("StartDedicatedAuthServer() error = %v, wantErr %v", err, tt.wantErr)
-				return
-			}
-			if !reflect.DeepEqual(gotSock, tt.wantSock) {
-				t.Errorf("StartDedicatedAuthServer() gotSock = %v, want %v", gotSock, tt.wantSock)
-			}
-			if !reflect.DeepEqual(gotServer, tt.wantServer) {
-				t.Errorf("StartDedicatedAuthServer() gotServer = %v, want %v", gotServer, tt.wantServer)
-			}
-			if !reflect.DeepEqual(gotAuthService, tt.wantAuthService) {
-				t.Errorf("StartDedicatedAuthServer() gotAuthService = %v, want %v", gotAuthService, tt.wantAuthService)
-			}
-		})
-	}
-}
-
-// mockStorage is a mocked persistence.Storage implementation that returns errors at the specified
-// operations.
-//
-// TODO(oxisto): Extract this struct into our new internal/testutils package
-type mockStorage struct {
-	createError error
-	saveError   error
-	updateError error
-	getError    error
-	listError   error
-	countError  error
-	deleteError error
-}
-
-func (m mockStorage) Create(interface{}) error { return m.createError }
-
-func (m mockStorage) Save(interface{}, ...interface{}) error { return m.saveError }
-
-func (m mockStorage) Update(interface{}, interface{}, ...interface{}) error {
-	return m.updateError
-}
-
-func (m mockStorage) Get(interface{}, ...interface{}) error { return m.getError }
-
-func (m mockStorage) List(interface{}, ...interface{}) error { return m.listError }
-
-func (m mockStorage) Count(interface{}, ...interface{}) (int64, error) {
-	return 0, m.countError
-}
-
-func (m mockStorage) Delete(interface{}, ...interface{}) error { return m.deleteError }
diff --git a/service/discovery/bufconn_test.go b/service/discovery/bufconn_test.go
index 9902110a..8c572872 100644
--- a/service/discovery/bufconn_test.go
+++ b/service/discovery/bufconn_test.go
@@ -30,10 +30,8 @@ import (
 	"net"
 
 	"clouditor.io/clouditor/api/assessment"
-	"clouditor.io/clouditor/api/auth"
 
 	service_assessment "clouditor.io/clouditor/service/assessment"
-	service_auth "clouditor.io/clouditor/service/auth"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/test/bufconn"
 )
@@ -50,15 +48,11 @@ func bufConnDialer(context.Context, string) (net.Conn, error) {
 
 // startBufConnServer starts an gRPC listening on a bufconn listener. It exposes
 // real functionality of the following services for testing purposes:
-// * Auth Service
 // * Assessment Service
-func startBufConnServer() (*grpc.Server, *service_auth.Service, *service_assessment.Service) {
+func startBufConnServer() (*grpc.Server, *service_assessment.Service) {
 	bufConnListener = bufconn.Listen(DefaultBufferSize)
 
 	server := grpc.NewServer()
-	// We do not want a persistent key storage here
-	authService := service_auth.NewService(service_auth.WithApiKeySaveOnCreate(false))
-	auth.RegisterAuthenticationServer(server, authService)
 
 	assessmentService := service_assessment.NewService()
 	assessment.RegisterAssessmentServer(server, assessmentService)
@@ -69,5 +63,5 @@ func startBufConnServer() (*grpc.Server, *service_auth.Service, *service_assessm
 		}
 	}()
 
-	return server, authService, assessmentService
+	return server, assessmentService
 }
diff --git a/service/discovery/discovery_test.go b/service/discovery/discovery_test.go
index d0cdb9b9..399767d8 100644
--- a/service/discovery/discovery_test.go
+++ b/service/discovery/discovery_test.go
@@ -38,8 +38,12 @@ import (
 	"clouditor.io/clouditor/api/assessment"
 	"clouditor.io/clouditor/api/discovery"
 	"clouditor.io/clouditor/api/evidence"
+	"clouditor.io/clouditor/internal/testutil"
+	"clouditor.io/clouditor/internal/testutil/clitest"
 	"clouditor.io/clouditor/voc"
+	oauth2 "github.com/oxisto/oauth2go"
 	"github.com/stretchr/testify/assert"
+	"golang.org/x/oauth2/clientcredentials"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/metadata"
@@ -48,17 +52,9 @@ import (
 )
 
 func TestMain(m *testing.M) {
-	// make sure, that we are in the clouditor root folder to find the policies
-	err := os.Chdir("../../")
-	if err != nil {
-		panic(err)
-	}
+	clitest.AutoChdir()
 
-	server, authService, _ := startBufConnServer()
-	err = authService.CreateDefaultUser("clouditor", "clouditor")
-	if err != nil {
-		panic(err)
-	}
+	server, _ := startBufConnServer()
 
 	code := m.Run()
 
@@ -252,6 +248,7 @@ func TestStart(t *testing.T) {
 	tests := []struct {
 		name           string
 		fields         fields
+		req            *discovery.StartDiscoveryRequest
 		providers      []string
 		wantResp       *discovery.StartDiscoveryResponse
 		wantErr        bool
@@ -395,6 +392,20 @@ func TestStart(t *testing.T) {
 }
 
 func TestService_initAssessmentStream(t *testing.T) {
+	var (
+		authSrv *oauth2.AuthorizationServer
+		port    int
+		err     error
+	)
+
+	authSrv, port, err = testutil.StartAuthenticationServer()
+	defer func() {
+		err = authSrv.Close()
+		assert.NoError(t, err)
+	}()
+
+	assert.NoError(t, err)
+
 	type fields struct {
 		hasRPCConnection bool
 		username         string
@@ -441,19 +452,19 @@ func TestService_initAssessmentStream(t *testing.T) {
 				password:         "password",
 			},
 			wantErr: func(tt assert.TestingT, err error, i ...interface{}) bool {
-				return assert.Equal(tt, err.Error(), "could not set up stream for assessing evidences: rpc error: code = Unauthenticated desc = transport: per-RPC creds failed due to error: error while logging in: rpc error: code = Unauthenticated desc = login failed")
+				s, _ := status.FromError(errors.Unwrap(err))
+				return assert.Equal(t, codes.Unauthenticated, s.Code())
 			},
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			s := NewService(WithInternalAuthorizer(
-				"bufnet",
-				tt.fields.username,
-				tt.fields.password,
-				grpc.WithContextDialer(bufConnDialer),
-			))
+			s := NewService(WithOAuth2Authorizer(&clientcredentials.Config{
+				ClientID:     testutil.TestAuthClientID,
+				ClientSecret: testutil.TestAuthClientSecret,
+				TokenURL:     testutil.TokenURL(port),
+			}))
 
 			var opts []grpc.DialOption
 			if tt.fields.hasRPCConnection {
diff --git a/service/orchestrator/orchestrator_test.go b/service/orchestrator/orchestrator_test.go
index e58355ca..004540a1 100644
--- a/service/orchestrator/orchestrator_test.go
+++ b/service/orchestrator/orchestrator_test.go
@@ -27,28 +27,28 @@ package orchestrator
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"fmt"
-	"github.com/google/uuid"
-	"google.golang.org/grpc"
 	"io"
-	"k8s.io/apimachinery/pkg/util/json"
 	"os"
 	"reflect"
 	"runtime"
 	"sync"
 	"testing"
 
-	"google.golang.org/grpc/metadata"
-	"google.golang.org/protobuf/types/known/emptypb"
-	"google.golang.org/protobuf/types/known/structpb"
-	"google.golang.org/protobuf/types/known/timestamppb"
-
 	"clouditor.io/clouditor/api/assessment"
-
 	"clouditor.io/clouditor/api/orchestrator"
+	"clouditor.io/clouditor/internal/testutil/clitest"
 	"clouditor.io/clouditor/persistence/inmemory"
+
+	"github.com/google/uuid"
 	"github.com/stretchr/testify/assert"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/metadata"
+	"google.golang.org/protobuf/types/known/emptypb"
+	"google.golang.org/protobuf/types/known/structpb"
+	"google.golang.org/protobuf/types/known/timestamppb"
 )
 
 const (
@@ -56,10 +56,7 @@ const (
 )
 
 func TestMain(m *testing.M) {
-	err := os.Chdir("../../")
-	if err != nil {
-		panic(err)
-	}
+	clitest.AutoChdir()
 
 	os.Exit(m.Run())
 }