Repository: clouditor/clouditor
Pull request #502 (2022-02-10 23:34:22)

Implementing refresh_token flow in CLI
This PR introduces the possibility to refresh the access token in the CLI using a refresh token. This also introduces a new token endpoint in the authentication server, that is compliant to the OAuth 2.0 standard (when used via the REST API). The CLI takes a little shortcut and uses this endpoint directly via gRPC.


Patch:
diff --git a/api/auth/auth.pb.go b/api/auth/auth.pb.go
index e1317d20..1cfc0f0d 100644
--- a/api/auth/auth.pb.go
+++ b/api/auth/auth.pb.go
@@ -184,25 +184,28 @@ func (x *LoginRequest) GetPassword() string {
 }
 
 // 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 LoginResponse struct {
+// 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
+	// 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".
+	// 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.
+	// 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 *LoginResponse) Reset() {
-	*x = LoginResponse{}
+func (x *TokenResponse) Reset() {
+	*x = TokenResponse{}
 	if protoimpl.UnsafeEnabled {
 		mi := &file_api_auth_auth_proto_msgTypes[2]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -210,13 +213,13 @@ func (x *LoginResponse) Reset() {
 	}
 }
 
-func (x *LoginResponse) String() string {
+func (x *TokenResponse) String() string {
 	return protoimpl.X.MessageStringOf(x)
 }
 
-func (*LoginResponse) ProtoMessage() {}
+func (*TokenResponse) ProtoMessage() {}
 
-func (x *LoginResponse) ProtoReflect() protoreflect.Message {
+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))
@@ -228,39 +231,110 @@ func (x *LoginResponse) ProtoReflect() protoreflect.Message {
 	return mi.MessageOf(x)
 }
 
-// Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead.
-func (*LoginResponse) Descriptor() ([]byte, []int) {
+// Deprecated: Use TokenResponse.ProtoReflect.Descriptor instead.
+func (*TokenResponse) Descriptor() ([]byte, []int) {
 	return file_api_auth_auth_proto_rawDescGZIP(), []int{2}
 }
 
-func (x *LoginResponse) GetAccessToken() string {
+func (x *TokenResponse) GetAccessToken() string {
 	if x != nil {
 		return x.AccessToken
 	}
 	return ""
 }
 
-func (x *LoginResponse) GetTokenType() string {
+func (x *TokenResponse) GetTokenType() string {
 	if x != nil {
 		return x.TokenType
 	}
 	return ""
 }
 
-func (x *LoginResponse) GetRefreshToken() string {
+func (x *TokenResponse) GetRefreshToken() string {
 	if x != nil {
 		return x.RefreshToken
 	}
 	return ""
 }
 
-func (x *LoginResponse) GetExpiry() *timestamppb.Timestamp {
+func (x *TokenResponse) GetExpiry() *timestamppb.Timestamp {
 	if x != nil {
 		return x.Expiry
 	}
 	return nil
 }
 
+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
@@ -270,7 +344,7 @@ type ListPublicKeysRequest struct {
 func (x *ListPublicKeysRequest) Reset() {
 	*x = ListPublicKeysRequest{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_api_auth_auth_proto_msgTypes[3]
+		mi := &file_api_auth_auth_proto_msgTypes[4]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -283,7 +357,7 @@ func (x *ListPublicKeysRequest) String() string {
 func (*ListPublicKeysRequest) ProtoMessage() {}
 
 func (x *ListPublicKeysRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_api_auth_auth_proto_msgTypes[3]
+	mi := &file_api_auth_auth_proto_msgTypes[4]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -296,7 +370,7 @@ func (x *ListPublicKeysRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use ListPublicKeysRequest.ProtoReflect.Descriptor instead.
 func (*ListPublicKeysRequest) Descriptor() ([]byte, []int) {
-	return file_api_auth_auth_proto_rawDescGZIP(), []int{3}
+	return file_api_auth_auth_proto_rawDescGZIP(), []int{4}
 }
 
 type ListPublicResponse struct {
@@ -310,7 +384,7 @@ type ListPublicResponse struct {
 func (x *ListPublicResponse) Reset() {
 	*x = ListPublicResponse{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_api_auth_auth_proto_msgTypes[4]
+		mi := &file_api_auth_auth_proto_msgTypes[5]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -323,7 +397,7 @@ func (x *ListPublicResponse) String() string {
 func (*ListPublicResponse) ProtoMessage() {}
 
 func (x *ListPublicResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_api_auth_auth_proto_msgTypes[4]
+	mi := &file_api_auth_auth_proto_msgTypes[5]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -336,7 +410,7 @@ func (x *ListPublicResponse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use ListPublicResponse.ProtoReflect.Descriptor instead.
 func (*ListPublicResponse) Descriptor() ([]byte, []int) {
-	return file_api_auth_auth_proto_rawDescGZIP(), []int{4}
+	return file_api_auth_auth_proto_rawDescGZIP(), []int{5}
 }
 
 func (x *ListPublicResponse) GetKeys() []*JsonWebKey {
@@ -346,28 +420,35 @@ func (x *ListPublicResponse) GetKeys() []*JsonWebKey {
 	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).
+// 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 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 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 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 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 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[5]
+		mi := &file_api_auth_auth_proto_msgTypes[6]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -380,7 +461,7 @@ func (x *JsonWebKey) String() string {
 func (*JsonWebKey) ProtoMessage() {}
 
 func (x *JsonWebKey) ProtoReflect() protoreflect.Message {
-	mi := &file_api_auth_auth_proto_msgTypes[5]
+	mi := &file_api_auth_auth_proto_msgTypes[6]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -393,7 +474,7 @@ func (x *JsonWebKey) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use JsonWebKey.ProtoReflect.Descriptor instead.
 func (*JsonWebKey) Descriptor() ([]byte, []int) {
-	return file_api_auth_auth_proto_rawDescGZIP(), []int{5}
+	return file_api_auth_auth_proto_rawDescGZIP(), []int{6}
 }
 
 func (x *JsonWebKey) GetKid() string {
@@ -453,7 +534,7 @@ var file_api_auth_auth_proto_rawDesc = []byte{
 	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, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73,
+	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,
@@ -464,34 +545,47 @@ var file_api_auth_auth_proto_rawDesc = []byte{
 	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, 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, 0xe0, 0x01, 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, 0x4c, 0x6f, 0x67, 0x69,
-	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, 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, 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,
 }
 
 var (
@@ -506,25 +600,28 @@ 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, 6)
+var file_api_auth_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
 var file_api_auth_auth_proto_goTypes = []interface{}{
 	(*User)(nil),                  // 0: clouditor.User
 	(*LoginRequest)(nil),          // 1: clouditor.LoginRequest
-	(*LoginResponse)(nil),         // 2: clouditor.LoginResponse
-	(*ListPublicKeysRequest)(nil), // 3: clouditor.ListPublicKeysRequest
-	(*ListPublicResponse)(nil),    // 4: clouditor.ListPublicResponse
-	(*JsonWebKey)(nil),            // 5: clouditor.JsonWebKey
-	(*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
+	(*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
 }
 var file_api_auth_auth_proto_depIdxs = []int32{
-	6, // 0: clouditor.LoginResponse.expiry:type_name -> google.protobuf.Timestamp
-	5, // 1: clouditor.ListPublicResponse.keys:type_name -> clouditor.JsonWebKey
+	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.ListPublicKeys:input_type -> clouditor.ListPublicKeysRequest
-	2, // 4: clouditor.Authentication.Login:output_type -> clouditor.LoginResponse
-	4, // 5: clouditor.Authentication.ListPublicKeys:output_type -> clouditor.ListPublicResponse
-	4, // [4:6] is the sub-list for method output_type
-	2, // [2:4] is the sub-list for method input_type
+	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
@@ -561,7 +658,7 @@ func file_api_auth_auth_proto_init() {
 			}
 		}
 		file_api_auth_auth_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*LoginResponse); i {
+			switch v := v.(*TokenResponse); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -573,7 +670,7 @@ func file_api_auth_auth_proto_init() {
 			}
 		}
 		file_api_auth_auth_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*ListPublicKeysRequest); i {
+			switch v := v.(*TokenRequest); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -585,7 +682,7 @@ func file_api_auth_auth_proto_init() {
 			}
 		}
 		file_api_auth_auth_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*ListPublicResponse); i {
+			switch v := v.(*ListPublicKeysRequest); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -597,6 +694,18 @@ func file_api_auth_auth_proto_init() {
 			}
 		}
 		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
@@ -615,7 +724,7 @@ func file_api_auth_auth_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_api_auth_auth_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   6,
+			NumMessages:   7,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
diff --git a/api/auth/auth.pb.gw.go b/api/auth/auth.pb.gw.go
index 273bf711..eea5f18a 100644
--- a/api/auth/auth.pb.gw.go
+++ b/api/auth/auth.pb.gw.go
@@ -65,6 +65,42 @@ func local_request_Authentication_Login_0(ctx context.Context, marshaler runtime
 
 }
 
+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
@@ -112,6 +148,29 @@ func RegisterAuthenticationHandlerServer(ctx context.Context, mux *runtime.Serve
 
 	})
 
+	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", runtime.WithHTTPPathPattern("/v1/auth/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()
@@ -196,6 +255,26 @@ func RegisterAuthenticationHandlerClient(ctx context.Context, mux *runtime.Serve
 
 	})
 
+	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", runtime.WithHTTPPathPattern("/v1/auth/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()
@@ -222,11 +301,15 @@ func RegisterAuthenticationHandlerClient(ctx context.Context, mux *runtime.Serve
 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 10bc15e0..49b9a00d 100644
--- a/api/auth/auth.proto
+++ b/api/auth/auth.proto
@@ -34,7 +34,7 @@ import "google/api/annotations.proto";
 option go_package = "api/auth";
 
 service Authentication {
-  rpc Login(LoginRequest) returns (LoginResponse) {
+  rpc Login(LoginRequest) returns (TokenResponse) {
     option (google.api.http) = {
       post : "/v1/auth/login"
       // TODO: Here, "*" is good since username and password (fields of
@@ -44,8 +44,17 @@ service Authentication {
     };
   };
 
-  // ListPublicKeys lists all available public keys of the authentication server
-  // in the JSON Web Key Set (JWKS) format.
+  // 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"
@@ -69,39 +78,62 @@ message LoginRequest {
 }
 
 // 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 LoginResponse {
-  // access_token contains the token which can be used to authenticate further calls to a resource endpoint
+// 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".
+  // 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.
+  // 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).
+// 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).
+  // 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).
+  // 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).
+  // 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).
+  // 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).
+  // 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
index a9678cd6..d1155b2b 100644
--- a/api/auth/auth_grpc.pb.go
+++ b/api/auth/auth_grpc.pb.go
@@ -22,9 +22,11 @@ const _ = grpc.SupportPackageIsVersion7
 //
 // 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) (*LoginResponse, error)
-	// ListPublicKeys lists all available public keys of the authentication server
-	// in the JSON Web Key Set (JWKS) format.
+	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)
 }
 
@@ -36,8 +38,8 @@ func NewAuthenticationClient(cc grpc.ClientConnInterface) AuthenticationClient {
 	return &authenticationClient{cc}
 }
 
-func (c *authenticationClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) {
-	out := new(LoginResponse)
+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
@@ -45,6 +47,15 @@ func (c *authenticationClient) Login(ctx context.Context, in *LoginRequest, opts
 	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...)
@@ -58,9 +69,11 @@ func (c *authenticationClient) ListPublicKeys(ctx context.Context, in *ListPubli
 // All implementations must embed UnimplementedAuthenticationServer
 // for forward compatibility
 type AuthenticationServer interface {
-	Login(context.Context, *LoginRequest) (*LoginResponse, error)
-	// ListPublicKeys lists all available public keys of the authentication server
-	// in the JSON Web Key Set (JWKS) format.
+	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()
 }
@@ -69,9 +82,12 @@ type AuthenticationServer interface {
 type UnimplementedAuthenticationServer struct {
 }
 
-func (UnimplementedAuthenticationServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) {
+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")
 }
@@ -106,6 +122,24 @@ func _Authentication_Login_Handler(srv interface{}, ctx context.Context, dec fun
 	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 {
@@ -135,6 +169,10 @@ var Authentication_ServiceDesc = grpc.ServiceDesc{
 			MethodName: "Login",
 			Handler:    _Authentication_Login_Handler,
 		},
+		{
+			MethodName: "Token",
+			Handler:    _Authentication_Token_Handler,
+		},
 		{
 			MethodName: "ListPublicKeys",
 			Handler:    _Authentication_ListPublicKeys_Handler,
diff --git a/service/authorizer.go b/api/authorizer.go
similarity index 70%
rename from service/authorizer.go
rename to api/authorizer.go
index 0893e1b4..3bdb1092 100644
--- a/service/authorizer.go
+++ b/api/authorizer.go
@@ -23,7 +23,7 @@
 //
 // This file is part of Clouditor Community Edition.
 
-package service
+package api
 
 import (
 	"context"
@@ -44,6 +44,11 @@ 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
@@ -56,15 +61,16 @@ type UsesAuthorizer interface {
 // 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
+// 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 {
-	Url string
+type internalAuthorizer struct {
+	authURL string
+
+	// grpcOptions contains additional grpc dial options
+	grpcOptions []grpc.DialOption
 
-	// GrpcOptions contains additional grpc dial options
-	GrpcOptions []grpc.DialOption
-	Username    string
-	Password    string
+	username string
+	password string
 
 	client auth.AuthenticationClient
 	conn   grpc.ClientConnInterface
@@ -76,8 +82,32 @@ type InternalAuthorizer struct {
 	tokenMutex sync.RWMutex
 }
 
+// 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,
+	}
+}
+
+// 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,
+	}
+}
+
 // init initializes the authorizer. This is called when fetching a token, if this authorizer has not been initialized.
-func (i *InternalAuthorizer) init() (err error) {
+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.
@@ -89,9 +119,9 @@ func (i *InternalAuthorizer) init() (err error) {
 
 	// 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...)
+	opts = append(opts, i.grpcOptions...)
 
-	if i.conn, err = grpc.Dial(i.Url, opts...); err != nil {
+	if i.conn, err = grpc.Dial(i.authURL, opts...); err != nil {
 		return fmt.Errorf("could not connect: %w", err)
 	}
 
@@ -103,10 +133,13 @@ func (i *InternalAuthorizer) init() (err error) {
 
 // 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
-// using the stored username / password credentials and refresh the access token, if it is expired.
-func (i *InternalAuthorizer) Token() (*oauth2.Token, error) {
+// (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.LoginResponse
+		resp *auth.TokenResponse
 		err  error
 	)
 
@@ -123,7 +156,7 @@ func (i *InternalAuthorizer) Token() (*oauth2.Token, error) {
 	// if another call is currently fetching a new one and thus modifying it.
 	i.tokenMutex.RLock()
 
-	// Check if we already have a token or if it is still ok
+	// 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()
@@ -139,10 +172,18 @@ func (i *InternalAuthorizer) Token() (*oauth2.Token, error) {
 	defer i.tokenMutex.Unlock()
 
 	// Otherwise, we need to re-authenticate
-	resp, err = i.client.Login(context.TODO(), &auth.LoginRequest{
-		Username: i.Username,
-		Password: i.Password,
-	})
+	// 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.
@@ -151,16 +192,23 @@ func (i *InternalAuthorizer) Token() (*oauth2.Token, error) {
 
 	// Store the current token, if login was successful
 	i.token = &oauth2.Token{
-		AccessToken: resp.AccessToken,
-		Expiry:      resp.Expiry.AsTime(),
+		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
+}
+
 // 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 (i *internalAuthorizer) 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()
 	if err != nil {
@@ -179,7 +227,7 @@ func (i *InternalAuthorizer) GetRequestMetadata(ctx context.Context, _ ...string
 	}, nil
 }
 
-func (*InternalAuthorizer) RequireTransportSecurity() bool {
+func (*internalAuthorizer) RequireTransportSecurity() bool {
 	// TODO(oxisto): This should be set to true because we transmit credentials (except localhost)
 	return false
 }
diff --git a/cli/commands/login/login.go b/cli/commands/login/login.go
index ce5423d6..12f824f7 100644
--- a/cli/commands/login/login.go
+++ b/cli/commands/login/login.go
@@ -29,10 +29,12 @@ import (
 	"context"
 	"fmt"
 
+	"clouditor.io/clouditor/api"
 	"clouditor.io/clouditor/api/auth"
 	"clouditor.io/clouditor/cli"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
+	"golang.org/x/oauth2"
 )
 
 const (
@@ -50,7 +52,7 @@ func NewLoginCommand() *cobra.Command {
 			var (
 				err     error
 				client  auth.AuthenticationClient
-				res     *auth.LoginResponse
+				res     *auth.TokenResponse
 				req     *auth.LoginRequest
 				session *cli.Session
 			)
@@ -76,8 +78,15 @@ func NewLoginCommand() *cobra.Command {
 				return fmt.Errorf("could not login: %w", err)
 			}
 
-			// update the session
-			session.Token = res.AccessToken
+			// 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 err = session.Save(); err != nil {
 				return fmt.Errorf("could not save session: %w", err)
diff --git a/cli/session.go b/cli/session.go
index ea31dc24..289d546e 100644
--- a/cli/session.go
+++ b/cli/session.go
@@ -35,13 +35,14 @@ import (
 	"os"
 	"strings"
 
+	"clouditor.io/clouditor/api"
 	"clouditor.io/clouditor/api/auth"
 	"clouditor.io/clouditor/api/orchestrator"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
+	"golang.org/x/oauth2"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials/insecure"
-	"google.golang.org/grpc/metadata"
 	"google.golang.org/grpc/status"
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/proto"
@@ -51,12 +52,20 @@ var DefaultSessionFolder string
 var Output io.Writer = os.Stdout
 
 type Session struct {
-	URL   string `json:"url"`
-	Token string `json:"token"`
+	*grpc.ClientConn
+	authorizer api.Authorizer
+
+	URL string `json:"url"`
 
 	Folder string `json:"-"`
+}
 
-	*grpc.ClientConn
+func (s *Session) SetAuthorizer(authorizer api.Authorizer) {
+	s.authorizer = authorizer
+}
+
+func (s *Session) Authorizer() api.Authorizer {
+	return s.authorizer
 }
 
 func init() {
@@ -71,16 +80,15 @@ func init() {
 	DefaultSessionFolder = fmt.Sprintf("%s/.clouditor/", home)
 }
 
-func NewSession(url string, opts ...grpc.DialOption) (session *Session, err error) {
+func NewSession(url string) (session *Session, err error) {
 	session = &Session{
 		URL:    url,
 		Folder: viper.GetString("session-directory"),
+		// We will supply the token later
+		authorizer: api.NewInternalAuthorizerFromToken(url, nil),
 	}
 
-	if len(opts) == 0 {
-		// TODO(oxisto): set flag depending on target url, insecure only for localhost
-		opts = []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
-	}
+	var opts = []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
 
 	if session.ClientConn, err = grpc.Dial(session.URL, opts...); err != nil {
 		return nil, fmt.Errorf("could not connect: %w", err)
@@ -113,7 +121,7 @@ func ContinueSession() (session *Session, err error) {
 		return
 	}
 
-	if session.ClientConn, err = grpc.Dial(session.URL, grpc.WithTransportCredentials(insecure.NewCredentials())); err != nil {
+	if session.ClientConn, err = grpc.Dial(session.URL, api.DefaultGrpcDialOptions(session)...); err != nil {
 		return nil, fmt.Errorf("could not connect: %w", err)
 	}
 
@@ -139,13 +147,48 @@ func (s *Session) Save() (err error) {
 		err = file.Close()
 	}(file)
 
-	if err = json.NewEncoder(file).Encode(s); err != nil {
+	if err = json.NewEncoder(file).Encode(&s); err != nil {
 		return fmt.Errorf("could not serialize JSON: %w", err)
 	}
 
 	return nil
 }
 
+// MarshalJSON is custom JSON marshalling implementation that gives us more control over
+// the fields we want to serialize. The core problem is that we want to serialize our token, e.g.
+// to store a session state for our clients, but we do not want to export the token field in our
+// struct. Exporting the field would create problems in multi-threaded environments. Therefore,
+// access is only allowed through the Token() function, which keeps the token synchronized using a mutex.
+func (s *Session) MarshalJSON() ([]byte, error) {
+	token, _ := s.authorizer.Token()
+
+	return json.Marshal(&struct {
+		URL   string        `json:"url"`
+		Token *oauth2.Token `json:"token"`
+	}{
+		URL:   s.URL,
+		Token: token,
+	})
+}
+
+// UnmarshalJSON is custom JSON marshalling implementation that gives us more control over
+// the fields we want to deserialize. See MarshalJSON for a detailed explanation, why this is
+// necessary.
+func (s *Session) UnmarshalJSON(data []byte) (err error) {
+	v := struct {
+		URL   string        `json:"url"`
+		Token *oauth2.Token `json:"token"`
+	}{}
+
+	if err = json.Unmarshal(data, &v); err != nil {
+		return
+	}
+
+	s.URL = v.URL
+	s.authorizer = api.NewInternalAuthorizerFromToken(s.URL, v.Token)
+	return
+}
+
 // HandleResponse handles the response and error message of an gRPC call
 func (*Session) HandleResponse(msg proto.Message, err error) error {
 	if err != nil {
@@ -200,27 +243,6 @@ func PromptForLogin() (loginRequest *auth.LoginRequest, err error) {
 	return loginRequest, nil
 }
 
-// Invoke implements `grpc.ClientConnInterface` and automatically provides an authenticated
-// context of this session
-func (s *Session) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error {
-	return s.ClientConn.Invoke(s.AuthenticatedContext(ctx), method, args, reply, opts...)
-}
-
-// NewStream implements `grpc.ClientConnInterface` and automatically provides an authenticated
-// context of this session
-func (s *Session) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
-	return s.ClientConn.NewStream(s.AuthenticatedContext(ctx), desc, method, opts...)
-}
-
-func (s *Session) AuthenticatedContext(ctx context.Context) context.Context {
-	return metadata.NewOutgoingContext(ctx,
-		metadata.Pairs(
-			"Authorization",
-			fmt.Sprintf("Bearer %s",
-				s.Token),
-		))
-}
-
 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 10f8d3f9..2e868163 100644
--- a/cmd/engine/engine.go
+++ b/cmd/engine/engine.go
@@ -26,9 +26,6 @@
 package main
 
 import (
-	"clouditor.io/clouditor/persistence"
-	"clouditor.io/clouditor/persistence/gorm"
-	"clouditor.io/clouditor/persistence/inmemory"
 	"context"
 	"errors"
 	"fmt"
@@ -37,6 +34,12 @@ import (
 	"os"
 	"strings"
 
+	"clouditor.io/clouditor/api"
+	"clouditor.io/clouditor/persistence"
+	"clouditor.io/clouditor/persistence/gorm"
+	"clouditor.io/clouditor/persistence/inmemory"
+	"clouditor.io/clouditor/service"
+
 	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"
@@ -51,7 +54,6 @@ import (
 	"clouditor.io/clouditor/api/orchestrator"
 	"clouditor.io/clouditor/cli"
 	"clouditor.io/clouditor/rest"
-	"clouditor.io/clouditor/service"
 
 	service_assessment "clouditor.io/clouditor/service/assessment"
 	service_auth "clouditor.io/clouditor/service/auth"
@@ -206,7 +208,7 @@ func doCmd(_ *cobra.Command, _ []string) (err error) {
 	)
 	discoveryService = service_discovery.NewService(
 		service_discovery.WithInternalAuthorizer(
-			service.DefaultInternalAuthorizerAddress,
+			api.DefaultInternalAuthorizerAddress,
 			viper.GetString(APIDefaultUserFlag),
 			viper.GetString(APIDefaultPasswordFlag),
 		),
@@ -214,7 +216,7 @@ func doCmd(_ *cobra.Command, _ []string) (err error) {
 	orchestratorService = service_orchestrator.NewService(service_orchestrator.WithStorage(db))
 	assessmentService = service_assessment.NewService(
 		service_assessment.WithInternalAuthorizer(
-			service.DefaultInternalAuthorizerAddress,
+			api.DefaultInternalAuthorizerAddress,
 			viper.GetString(APIDefaultUserFlag),
 			viper.GetString(APIDefaultPasswordFlag),
 		),
diff --git a/rest/rest.go b/rest/rest.go
index cbd22309..182d35c7 100644
--- a/rest/rest.go
+++ b/rest/rest.go
@@ -41,6 +41,8 @@ import (
 	"clouditor.io/clouditor/api/discovery"
 	"clouditor.io/clouditor/api/evidence"
 	"clouditor.io/clouditor/api/orchestrator"
+	service_auth "clouditor.io/clouditor/service/auth"
+
 	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
 	"github.com/sirupsen/logrus"
 	"google.golang.org/grpc"
@@ -138,7 +140,9 @@ func RunServer(ctx context.Context, grpcPort int, httpPort int, serverOpts ...Se
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 
-	mux := runtime.NewServeMux()
+	mux := runtime.NewServeMux(
+		runtime.WithErrorHandler(service_auth.OAuthErrorHandler),
+	)
 
 	for _, o := range serverOpts {
 		o(cors, mux)
diff --git a/service/assessment/assessment.go b/service/assessment/assessment.go
index 97f72f3e..74d6455b 100644
--- a/service/assessment/assessment.go
+++ b/service/assessment/assessment.go
@@ -32,11 +32,11 @@ import (
 	"io"
 	"sync"
 
+	"clouditor.io/clouditor/api"
 	"clouditor.io/clouditor/api/assessment"
 	"clouditor.io/clouditor/api/evidence"
 	"clouditor.io/clouditor/api/orchestrator"
 	"clouditor.io/clouditor/policies"
-	"clouditor.io/clouditor/service"
 	"github.com/google/uuid"
 	"github.com/sirupsen/logrus"
 	"google.golang.org/grpc"
@@ -76,14 +76,14 @@ type Service struct {
 	// Currently, results are just stored as a map (=in-memory). In the future, we will use a DB.
 	results map[string]*assessment.AssessmentResult
 
-	authorizer service.Authorizer
+	authorizer api.Authorizer
 }
 
 const (
 	// DefaultEvidenceStoreAddress specifies the default gRPC address of the evidence store.
 	DefaultEvidenceStoreAddress = "localhost:9090"
 
-	// DefaultEvidenceStoreAddress specifies the default gRPC address of the orchestrator.
+	// DefaultOrchestratorAddress specifies the default gRPC address of the orchestrator.
 	DefaultOrchestratorAddress = "localhost:9090"
 )
 
@@ -97,7 +97,7 @@ func WithEvidenceStoreAddress(address string) ServiceOption {
 	}
 }
 
-// WithEvidenceStoreAddress is an option to configure the orchestrator gRPC address.
+// WithOrchestratorAddress is an option to configure the orchestrator gRPC address.
 func WithOrchestratorAddress(address string) ServiceOption {
 	return func(s *Service) {
 		s.orchestratorAddress = address
@@ -107,12 +107,7 @@ 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 {
 	return func(s *Service) {
-		s.SetAuthorizer(&service.InternalAuthorizer{
-			Url:         address,
-			GrpcOptions: opts,
-			Username:    username,
-			Password:    password,
-		})
+		s.SetAuthorizer(api.NewInternalAuthorizerFromPassword(address, username, password, opts...))
 	}
 }
 
@@ -133,12 +128,12 @@ func NewService(opts ...ServiceOption) *Service {
 }
 
 // SetAuthorizer implements UsesAuthorizer
-func (s *Service) SetAuthorizer(auth service.Authorizer) {
+func (s *Service) SetAuthorizer(auth api.Authorizer) {
 	s.authorizer = auth
 }
 
 // Authorizer implements UsesAuthorizer
-func (s *Service) Authorizer() service.Authorizer {
+func (s *Service) Authorizer() api.Authorizer {
 	return s.authorizer
 }
 
@@ -321,7 +316,7 @@ func (s *Service) initEvidenceStoreStream(additionalOpts ...grpc.DialOption) err
 
 	// Establish connection to evidence store gRPC service
 	conn, err := grpc.Dial(s.evidenceStoreAddress,
-		service.DefaultGrpcDialOptions(s, additionalOpts...)...,
+		api.DefaultGrpcDialOptions(s, additionalOpts...)...,
 	)
 	if err != nil {
 		return fmt.Errorf("could not connect to evidence store service: %w", err)
@@ -360,7 +355,7 @@ func (s *Service) initOrchestratorStream(additionalOpts ...grpc.DialOption) erro
 
 	// Establish connection to orchestrator gRPC service
 	conn, err := grpc.Dial(s.orchestratorAddress,
-		service.DefaultGrpcDialOptions(s, additionalOpts...)...,
+		api.DefaultGrpcDialOptions(s, additionalOpts...)...,
 	)
 	if err != nil {
 		return fmt.Errorf("could not connect to orchestrator service: %w", err)
diff --git a/service/auth/auth.go b/service/auth/auth.go
index a13d0ee1..950b323c 100644
--- a/service/auth/auth.go
+++ b/service/auth/auth.go
@@ -34,6 +34,7 @@ import (
 	"errors"
 	"fmt"
 	"io/ioutil"
+	"net/http"
 	"os"
 	"os/user"
 	"path/filepath"
@@ -46,16 +47,13 @@ import (
 	"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"
 )
 
-const (
-	Issuer = "clouditor"
-)
-
 var log *logrus.Entry
 
 func init() {
@@ -75,6 +73,12 @@ const (
 
 	// 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
@@ -194,7 +198,12 @@ func loadApiKey(path string, password []byte) (key *ecdsa.PrivateKey, err error)
 	if err != nil {
 		return nil, fmt.Errorf("error while opening the file: %w", err)
 	}
-	defer f.Close()
+	defer func() {
+		err := f.Close()
+		if err != nil {
+			log.Errorf("Error while closing file: %v", err)
+		}
+	}()
 
 	data, err := ioutil.ReadAll(f)
 	if err != nil {
@@ -265,11 +274,16 @@ func (s *Service) recoverFromLoadApiKeyError(err error, defaultPath bool) {
 }
 
 // Login handles a login request
-func (s Service) Login(_ context.Context, request *auth.LoginRequest) (response *auth.LoginResponse, err error) {
-	var result bool
-	var u *auth.User
+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, u, err = s.verifyLogin(request); err != nil {
+	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)
 	}
@@ -279,23 +293,80 @@ func (s Service) Login(_ context.Context, request *auth.LoginRequest) (response
 		return nil, status.Errorf(codes.Unauthenticated, "login failed")
 	}
 
-	var token string
+	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)
+	}
 
-	var expiry = time.Now().Add(time.Hour * 24)
+	response = &auth.TokenResponse{
+		AccessToken:  token,
+		RefreshToken: refreshToken,
+		TokenType:    "Bearer",
+		Expiry:       timestamppb.New(expiry),
+	}
 
-	if token, err = s.issueToken(u.Username, u.FullName, u.Email, expiry); err != nil {
-		return nil, status.Errorf(codes.Internal, "token issue failed: %v", err)
+	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")
 	}
 
-	response = &auth.LoginResponse{
+	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),
-	}
-
-	return response, nil
+	}, 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{
@@ -387,20 +458,32 @@ func (s *Service) CreateDefaultUser(username string, password string) error {
 }
 
 // issueToken issues a JWT-based token
-func (s Service) issueToken(subject string, fullName string, email string, expiry time.Time) (token string, err error) {
+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:    Issuer,
+				Issuer:    DefaultIssuer,
 				Subject:   subject,
 			}},
 	)
 	claims.Header["kid"] = DefaultKeyID
 
-	token, err = claims.SignedString(s.apiKey)
+	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
 }
 
@@ -438,3 +521,24 @@ func expandPath(path string) (out string, err error) {
 
 	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/pem.go b/service/auth/pem.go
index f7ea66d5..4d99ac7f 100644
--- a/service/auth/pem.go
+++ b/service/auth/pem.go
@@ -50,48 +50,56 @@ var (
 
 var ErrNotECPrivateKey = errors.New("key is not a valid EC private key")
 
-// See https://datatracker.ietf.org/doc/html/rfc8018#appendix-A.2
+// PBKDF2Params are parameters for PBKDF2. See https://datatracker.ietf.org/doc/html/rfc8018#appendix-A.2.
 type PBKDF2Params struct {
 	Salt           []byte
 	IterationCount int
 	PRF            asn1.ObjectIdentifier `asn1:"optional"`
 }
 
-// Part of PBES2. See https://datatracker.ietf.org/doc/html/rfc8018#appendix-A.4
+// KeyDerivationFunc is part of PBES2 and specify the key derivation function.
+// See https://datatracker.ietf.org/doc/html/rfc8018#appendix-A.4.
 type KeyDerivationFunc struct {
 	Algorithm    asn1.ObjectIdentifier
 	PBKDF2Params PBKDF2Params
 }
 
-// Part of PBES2. See https://datatracker.ietf.org/doc/html/rfc8018#appendix-A.4
+// EncryptionScheme is part of PBES2 and specifies the encryption algorithm.
+// See https://datatracker.ietf.org/doc/html/rfc8018#appendix-A.4.
 type EncryptionScheme struct {
 	EncryptionAlgorithm asn1.ObjectIdentifier
 	IV                  []byte
 }
 
-// See https://datatracker.ietf.org/doc/html/rfc8018#appendix-A.4
+// PBES2Params are parameters for PBES2. See https://datatracker.ietf.org/doc/html/rfc8018#appendix-A.4.
 type PBES2Params struct {
 	KeyDerivationFunc KeyDerivationFunc
 	EncryptionScheme  EncryptionScheme
 }
 
-// See https://datatracker.ietf.org/doc/html/rfc5958#section-3
+// EncryptionAlgorithmIdentifier is the identifier for the encryption algorithm.
+// See https://datatracker.ietf.org/doc/html/rfc5958#section-3.
 type EncryptionAlgorithmIdentifier struct {
 	Algorithm asn1.ObjectIdentifier
 	Params    PBES2Params
 }
 
-// See https://datatracker.ietf.org/doc/html/rfc5958#section-3
+// EncryptedPrivateKeyInfo contains meta-info about the encrypted private key.
+// See https://datatracker.ietf.org/doc/html/rfc5958#section-3.
 type EncryptedPrivateKeyInfo struct {
 	EncryptionAlgorithm EncryptionAlgorithmIdentifier
 	EncryptedData       []byte
 }
 
+// MarshalECPrivateKeyWithPassword marshals an ECDSA private key protected with a password
+// according to PKCS#8 into a byte array
 func MarshalECPrivateKeyWithPassword(key *ecdsa.PrivateKey, password []byte) (data []byte, err error) {
 	var decrypted []byte
 	decrypted, err = x509.MarshalPKCS8PrivateKey(key)
 	if err != nil {
-		return nil, err // directly return error here
+		// Directly return error here, because we are basically a wrapper around
+		// x509.MarshalPKCS8PrivateKey and we want our errors to be similar
+		return nil, err
 	}
 
 	block, err := EncryptPEMBlock(rand.Reader, decrypted, password)
@@ -102,6 +110,8 @@ func MarshalECPrivateKeyWithPassword(key *ecdsa.PrivateKey, password []byte) (da
 	return pem.EncodeToMemory(block), nil
 }
 
+// ParseECPrivateKeyFromPEMWithPassword ready an ECDSA private key protected with a password according
+// to PKCS#8 from a byte array.
 func ParseECPrivateKeyFromPEMWithPassword(data []byte, password []byte) (key *ecdsa.PrivateKey, err error) {
 	// Parse PEM block
 	var block *pem.Block
@@ -110,13 +120,15 @@ func ParseECPrivateKeyFromPEMWithPassword(data []byte, password []byte) (key *ec
 	}
 
 	var decrypted []byte
-	if decrypted, err = DecryptPEMBlock(block, []byte(password)); err != nil {
+	if decrypted, err = DecryptPEMBlock(block, password); err != nil {
 		return nil, fmt.Errorf("could not decrypt PEM block: %w", err)
 	}
 
 	parsedKey, err := x509.ParsePKCS8PrivateKey(decrypted)
 	if err != nil {
-		return nil, err // directly return error here
+		// Directly return error here, because we are basically a wrapper around
+		// x509.ParsePKCS8PrivateKey and we want our errors to be similar
+		return nil, err
 	}
 
 	var ok bool
@@ -127,6 +139,7 @@ func ParseECPrivateKeyFromPEMWithPassword(data []byte, password []byte) (key *ec
 	return
 }
 
+// EncryptPEMBlock encrypts a private key contain in data into a PEM block according to PKCS#8.
 func EncryptPEMBlock(rand io.Reader, data, password []byte) (block *pem.Block, err error) {
 	var salt = make([]byte, 8)
 	if _, err = rand.Read(salt); err != nil {
diff --git a/service/discovery/discovery.go b/service/discovery/discovery.go
index d1f6ae86..20117753 100644
--- a/service/discovery/discovery.go
+++ b/service/discovery/discovery.go
@@ -27,11 +27,10 @@ package discovery
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"time"
 
-	"clouditor.io/clouditor/service"
+	"clouditor.io/clouditor/api"
 	"clouditor.io/clouditor/service/discovery/azure"
 	"clouditor.io/clouditor/service/discovery/k8s"
 	"google.golang.org/protobuf/types/known/timestamppb"
@@ -54,10 +53,6 @@ import (
 
 var log *logrus.Entry
 
-var (
-	ErrNoAuthorizerConfigured = errors.New("no authorizer configured")
-)
-
 // Service is an implementation of the Clouditor Discovery service.
 // It should not be used directly, but rather the NewService constructor
 // should be used.
@@ -72,7 +67,7 @@ type Service struct {
 	resources map[string]voc.IsCloudResource
 	scheduler *gocron.Scheduler
 
-	authorizer service.Authorizer
+	authorizer api.Authorizer
 }
 
 type Configuration struct {
@@ -101,12 +96,7 @@ 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 {
 	return func(s *Service) {
-		s.SetAuthorizer(&service.InternalAuthorizer{
-			Url:         address,
-			GrpcOptions: opts,
-			Username:    username,
-			Password:    password,
-		})
+		s.SetAuthorizer(api.NewInternalAuthorizerFromPassword(address, username, password, opts...))
 	}
 }
 
@@ -127,12 +117,12 @@ func NewService(opts ...ServiceOption) *Service {
 }
 
 // SetAuthorizer implements UsesAuthorizer.
-func (s *Service) SetAuthorizer(auth service.Authorizer) {
+func (s *Service) SetAuthorizer(auth api.Authorizer) {
 	s.authorizer = auth
 }
 
 // Authorizer implements UsesAuthorizer.
-func (s *Service) Authorizer() service.Authorizer {
+func (s *Service) Authorizer() api.Authorizer {
 	return s.authorizer
 }
 
@@ -143,7 +133,7 @@ func (s *Service) initAssessmentStream(additionalOpts ...grpc.DialOption) error
 
 	// Establish connection to assessment gRPC service
 	conn, err := grpc.Dial(s.assessmentAddress,
-		service.DefaultGrpcDialOptions(s, additionalOpts...)...,
+		api.DefaultGrpcDialOptions(s, additionalOpts...)...,
 	)
 	if err != nil {
 		return fmt.Errorf("could not connect to assessment service: %w", err)


Test patch:
diff --git a/api/authorizer_test.go b/api/authorizer_test.go
new file mode 100644
index 00000000..38a4c72d
--- /dev/null
+++ b/api/authorizer_test.go
@@ -0,0 +1,189 @@
+// 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 api
+
+import (
+	"context"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"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"
+)
+
+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)
+
+	// Create a refresh token
+	claims := jwt.NewWithClaims(jwt.SigningMethodES256, &jwt.RegisteredClaims{
+		Subject: "clouditor",
+	})
+
+	mockRefreshToken, _ = claims.SignedString(tmpKey)
+
+	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),
+	})
+
+	mockAccessToken, _ = claims.SignedString(tmpKey)
+}
+
+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
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		want    *oauth2.Token
+		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",
+			fields: fields{
+				client: &mockAuthClient{},
+				conn:   &mockConn{},
+				token: &oauth2.Token{
+					AccessToken: mockAccessToken,
+					TokenType:   "Bearer",
+					Expiry:      mockExpiry,
+				},
+			},
+			want: &oauth2.Token{
+				AccessToken: mockAccessToken,
+				Expiry:      mockExpiry,
+				TokenType:   "Bearer",
+			},
+		},
+	}
+
+	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,
+			}
+			got, err := i.Token()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("InternalAuthorizer.Token() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("InternalAuthorizer.Token() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+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 (mockConn) Invoke(_ context.Context, _ string, _ interface{}, _ interface{}, _ ...grpc.CallOption) error {
+	return nil
+}
+
+func (mockConn) NewStream(_ context.Context, _ *grpc.StreamDesc, _ string, _ ...grpc.CallOption) (grpc.ClientStream, error) {
+	return nil, nil
+}
diff --git a/cli/session_test.go b/cli/session_test.go
index 1a0d64c2..0aae6c72 100644
--- a/cli/session_test.go
+++ b/cli/session_test.go
@@ -33,16 +33,20 @@ import (
 	"os"
 	"testing"
 
+	"clouditor.io/clouditor/api"
+	"clouditor.io/clouditor/api/auth"
+	"clouditor.io/clouditor/api/orchestrator"
 	"clouditor.io/clouditor/service"
+	service_auth "clouditor.io/clouditor/service/auth"
+	service_orchestrator "clouditor.io/clouditor/service/orchestrator"
 
-	"google.golang.org/protobuf/proto"
-
-	"clouditor.io/clouditor/api/auth"
 	"github.com/spf13/viper"
 	"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 (
@@ -58,7 +62,10 @@ func TestMain(m *testing.M) {
 	if err != nil {
 		panic(err)
 	}
-	sock, server, _, err = service.StartDedicatedAuthServer(":0")
+
+	s := service_orchestrator.NewService()
+	sock, server, _, err = service.StartDedicatedAuthServer(":0", service_auth.WithApiKeySaveOnCreate(false))
+	orchestrator.RegisterOrchestratorServer(server, s)
 	if err != nil {
 		panic(err)
 	}
@@ -93,7 +100,7 @@ func TestSession(t *testing.T) {
 
 	client := auth.NewAuthenticationClient(session)
 
-	var response *auth.LoginResponse
+	var response *auth.TokenResponse
 
 	// login with real user
 	response, err = client.Login(context.Background(), &auth.LoginRequest{Username: "clouditor", Password: "clouditor"})
@@ -103,7 +110,15 @@ func TestSession(t *testing.T) {
 	assert.NotEmpty(t, response.AccessToken)
 
 	// update the session
-	session.Token = response.AccessToken
+	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()
 
@@ -113,25 +128,16 @@ func TestSession(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotNil(t, session)
 
-	client = auth.NewAuthenticationClient(session)
-
-	// login with non-existing user
-	// TODO(oxisto): Should be moved to a service/auth test. here we should only test the session mechanism
-	response, err = client.Login(context.Background(), &auth.LoginRequest{Username: "some-other-user", Password: "password"})
-
-	assert.Error(t, err)
-
-	s, ok := status.FromError(err)
-
-	assert.True(t, ok)
-	assert.Equal(t, codes.Unauthenticated, s.Code())
-	assert.Nil(t, response)
+	// Do a simple authenticated call
+	oc := orchestrator.NewOrchestratorClient(session)
+	_, err = oc.ListCloudServices(context.Background(), &orchestrator.ListCloudServicesRequest{})
+	assert.NoError(t, err)
 }
 
 func TestSession_HandleResponse(t *testing.T) {
 	type fields struct {
 		URL        string
-		Token      string
+		Token      oauth2.Token
 		Folder     string
 		ClientConn *grpc.ClientConn
 	}
@@ -179,8 +185,8 @@ func TestSession_HandleResponse(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			s := &Session{
-				URL:        tt.fields.URL,
-				Token:      tt.fields.Token,
+				URL: tt.fields.URL,
+				//Token:      tt.fields.Token,
 				Folder:     tt.fields.Folder,
 				ClientConn: tt.fields.ClientConn,
 			}
diff --git a/rest/rest_test.go b/rest/rest_test.go
index 3186b432..c45424aa 100644
--- a/rest/rest_test.go
+++ b/rest/rest_test.go
@@ -26,9 +26,11 @@
 package rest
 
 import (
+	"bytes"
 	"context"
 	"errors"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"net"
 	"net/http"
@@ -112,10 +114,12 @@ func TestREST(t *testing.T) {
 	assert.NotEqual(t, 0, port)
 
 	type args struct {
-		origin    string
-		method    string
-		url       string
-		preflight bool
+		origin      string
+		contentType string
+		method      string
+		url         string
+		body        io.Reader
+		preflight   bool
 	}
 	tests := []struct {
 		name         string
@@ -194,11 +198,36 @@ func TestREST(t *testing.T) {
 				}
 
 				content, err := ioutil.ReadAll(resp.Body)
-				if assert.ErrorIs(tt, err, nil) {
+				if !assert.ErrorIs(tt, err, nil) {
 					return false
 				}
 
-				return assert.Equal(tt, content, []byte("just a test"))
+				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)
 			},
 		},
 	}
@@ -214,11 +243,12 @@ func TestREST(t *testing.T) {
 				method = tt.args.method
 			}
 
-			req, err := http.NewRequest(method, fmt.Sprintf("http://localhost:%d/%s", port, tt.args.url), nil)
+			req, err := http.NewRequest(method, fmt.Sprintf("http://localhost:%d/%s", port, tt.args.url), tt.args.body)
 			assert.NoError(t, err)
 			assert.NotNil(t, req)
 
 			req.Header.Add("Origin", tt.args.origin)
+			req.Header.Add("Content-Type", tt.args.contentType)
 
 			if tt.args.preflight {
 				req.Header.Add("Access-Control-Request-Method", tt.args.method)
diff --git a/service/auth/auth_test.go b/service/auth/auth_test.go
index c419814b..7e46b928 100644
--- a/service/auth/auth_test.go
+++ b/service/auth/auth_test.go
@@ -104,6 +104,76 @@ func TestService_ListPublicKeys(t *testing.T) {
 	}
 }
 
+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