diff --git a/Makefile b/Makefile index 874e20c1f..68deda976 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ CERT_TOOL_SIGN_ALG ?= ECDSA-SHA256 CERT_TOOL_ELLIPTIC_CURVE ?= P256 CERT_TOOL_IMAGE = ghcr.io/plgd-dev/hub/cert-tool:vnext -SUBDIRS := bundle certificate-authority cloud2cloud-connector cloud2cloud-gateway coap-gateway grpc-gateway resource-aggregate resource-directory http-gateway identity-store snippet-service test/oauth-server tools/cert-tool +SUBDIRS := bundle certificate-authority cloud2cloud-connector cloud2cloud-gateway coap-gateway grpc-gateway resource-aggregate resource-directory http-gateway identity-store snippet-service m2m-oauth-server test/oauth-server tools/cert-tool .PHONY: $(SUBDIRS) push proto/generate clean build test env mongo nats certificates hub-build http-gateway-www simulators default: build diff --git a/certificate-authority/config.yaml b/certificate-authority/config.yaml index 6460ad5cb..cc21c1e61 100644 --- a/certificate-authority/config.yaml +++ b/certificate-authority/config.yaml @@ -44,6 +44,9 @@ apis: keyFile: "/secrets/private/cert.key" certFile: "/secrets/public/cert.crt" useSystemCAPool: false + tokenTrustVerification: + enabled: true + cacheExpiration: 30s http: address: "0.0.0.0:9101" readTimeout: 8s @@ -112,5 +115,3 @@ signer: certFile: "/secrets/public/intermediateca.crt" validFrom: "now-1h" expiresIn: "87600h" - - diff --git a/certificate-authority/service/cleanDatabase_test.go b/certificate-authority/service/cleanDatabase_test.go index 29ba16dbb..afe8d2c87 100644 --- a/certificate-authority/service/cleanDatabase_test.go +++ b/certificate-authority/service/cleanDatabase_test.go @@ -29,7 +29,7 @@ func TestCertificateAuthorityServerCleanUpSigningRecords(t *testing.T) { cfg.Clients.Storage.CleanUpRecords = "*/1 * * * * *" fmt.Printf("%v\n\n", test.MakeConfig(t)) - shutDown := testService.SetUpServices(context.Background(), t, testService.SetUpServicesCertificateAuthority|testService.SetUpServicesOAuth, testService.WithCAConfig(cfg)) + shutDown := testService.SetUpServices(context.Background(), t, testService.SetUpServicesCertificateAuthority|testService.SetUpServicesOAuth|testService.SetUpServicesMachine2MachineOAuth, testService.WithCAConfig(cfg)) defer shutDown() storeDB, closeStore := test.NewStore(t) diff --git a/charts/plgd-hub/templates/_helpers.tpl b/charts/plgd-hub/templates/_helpers.tpl index 29317eea4..5472fbc0d 100644 --- a/charts/plgd-hub/templates/_helpers.tpl +++ b/charts/plgd-hub/templates/_helpers.tpl @@ -220,6 +220,15 @@ tls: {{- include "plgd-hub.httpConfig" (list $ .http $certPath ) | indent 8 }} {{- end }} {{- end }} + tokenTrustVerification: + {{- $tokenTrustVerification := $authorization.tokenTrustVerification }} + {{- if not $tokenTrustVerification }} + {{- $tokenTrustVerification = $.Values.global.authorization.tokenTrustVerification }} + {{- end }} + enabled: {{ $tokenTrustVerification.enabled | default false }} + {{- if $tokenTrustVerification.enabled }} + cacheExpiration: {{ $tokenTrustVerification.cacheExpiration }} + {{- end }} {{- end }} {{- define "plgd-hub.authorizationConfig" }} diff --git a/charts/plgd-hub/templates/certificate-authority/config.yaml b/charts/plgd-hub/templates/certificate-authority/config.yaml index 67bde2980..a72d72326 100644 --- a/charts/plgd-hub/templates/certificate-authority/config.yaml +++ b/charts/plgd-hub/templates/certificate-authority/config.yaml @@ -30,11 +30,11 @@ data: # 0s - means infinity maxConnectionIdle: {{ .apis.grpc.keepAlive.maxConnectionIdle }} # 0s - means infinity - maxConnectionAge: {{ .apis.grpc.keepAlive.maxConnectionIdle }} + maxConnectionAge: {{ .apis.grpc.keepAlive.maxConnectionAge }} # 0s - means infinity maxConnectionAgeGrace: {{ .apis.grpc.keepAlive.maxConnectionAgeGrace }} - time: {{ .apis.grpc.keepAlive.maxConnectionIdle }} - timeout: {{ .apis.grpc.keepAlive.maxConnectionIdle }} + time: {{ .apis.grpc.keepAlive.time }} + timeout: {{ .apis.grpc.keepAlive.timeout }} tls: {{- $tls := .apis.grpc.tls }} {{- include "plgd-hub.internalCertificateConfig" (list $ $tls $cert ) | indent 8 }} diff --git a/charts/plgd-hub/templates/m2m-oauth-server/_helpers.tpl b/charts/plgd-hub/templates/m2m-oauth-server/_helpers.tpl index 0086309d3..1d5a02b9a 100644 --- a/charts/plgd-hub/templates/m2m-oauth-server/_helpers.tpl +++ b/charts/plgd-hub/templates/m2m-oauth-server/_helpers.tpl @@ -17,7 +17,7 @@ {{- end -}} {{- define "plgd-hub.m2moauthserver.createServiceCertByCm" }} - {{- $serviceTls := .Values.m2moauthserver.apis.http.tls.certFile }} + {{- $serviceTls := .Values.m2moauthserver.apis.grpc.tls.certFile }} {{- if $serviceTls }} {{- printf "" -}} {{- else }} @@ -78,14 +78,6 @@ true {{- end }} {{- end }} -{{- define "plgd-hub.m2moauthserver.clientServiceSecretEnabled" -}} -{{- if or .Values.global.m2mOAuthServer.clientServiceSecret .Values.m2moauthserver.clientServiceSecret.enabled }} -true -{{- else }} -{{- printf "" }} -{{- end }} -{{- end }} - {{- define "plgd-hub.m2moauthserver.getPrivateKeyFile" -}} {{- $privateKeyFile := .Values.m2moauthserver.oauthSigner.privateKeyFile }} {{- if and (not $privateKeyFile) (include "plgd-hub.m2moauthserver.privateKeySecretEnabled" $) }} @@ -94,15 +86,6 @@ true {{- printf "%s" $privateKeyFile }} {{- end -}} -{{- define "plgd-hub.m2moauthserver.getClientServiceSecretFile" -}} -{{- $file := "" }} -{{- if include "plgd-hub.m2moauthserver.clientServiceSecretEnabled" $ }} -{{- $file = printf "%s/%s" .Values.m2moauthserver.clientServiceSecret.mountPath .Values.m2moauthserver.clientServiceSecret.fileName }} -{{- end }} -{{- printf "%s" $file }} -{{- end -}} - - {{- define "plgd-hub.m2moauthserver.enabled" -}} {{- if and .Values.m2moauthserver.enabled (include "plgd-hub.m2moauthserver.privateKeySecretEnabled" .) }} true diff --git a/charts/plgd-hub/templates/m2m-oauth-server/config.yaml b/charts/plgd-hub/templates/m2m-oauth-server/config.yaml index a149a9d80..128c24d56 100644 --- a/charts/plgd-hub/templates/m2m-oauth-server/config.yaml +++ b/charts/plgd-hub/templates/m2m-oauth-server/config.yaml @@ -1,5 +1,5 @@ {{- if include "plgd-hub.m2moauthserver.enabled" . }} -{{- $oauthServerCertPath := "/certs" }} +{{- $cert := "/certs" }} apiVersion: v1 kind: ConfigMap metadata: @@ -17,16 +17,49 @@ data: encoderConfig: timeEncoder: {{ .log.encoderConfig.timeEncoder }} apis: + grpc: + address: {{ .apis.grpc.address | default (printf "0.0.0.0:%v" .port) | quote }} + sendMsgSize: {{ int64 .apis.grpc.sendMsgSize | default 4194304 }} + recvMsgSize: {{ int64 .apis.grpc.recvMsgSize | default 4194304 }} + enforcementPolicy: + minTime: {{ .apis.grpc.enforcementPolicy.minTime }} + permitWithoutStream: {{ .apis.grpc.enforcementPolicy.permitWithoutStream }} + keepAlive: + # 0s - means infinity + maxConnectionIdle: {{ .apis.grpc.keepAlive.maxConnectionIdle }} + # 0s - means infinity + maxConnectionAge: {{ .apis.grpc.keepAlive.maxConnectionAge }} + # 0s - means infinity + maxConnectionAgeGrace: {{ .apis.grpc.keepAlive.maxConnectionAgeGrace }} + time: {{ .apis.grpc.keepAlive.time }} + timeout: {{ .apis.grpc.keepAlive.timeout }} + tls: + {{- $tls := .apis.grpc.tls }} + {{- include "plgd-hub.internalCertificateConfig" (list $ $tls $cert ) | indent 8 }} + clientCertificateRequired: {{ .apis.grpc.tls.clientCertificateRequired }} + authorization: + {{- $authorization := .apis.grpc.authorization }} + {{- include "plgd-hub.authorizationConfig" (list $ $authorization "m2moauthserver.apis.grpc.authorization" $cert ) | indent 8 }} http: - address: {{ .apis.http.address | default (printf "0.0.0.0:%v" .port) | quote }} + address: {{ .apis.http.address | default (printf "0.0.0.0:%v" .httpPort) | quote }} readTimeout: {{ .apis.http.readTimeout }} readHeaderTimeout: {{ .apis.http.readHeaderTimeout }} writeTimeout: {{ .apis.http.writeTimeout }} idleTimeout: {{ .apis.http.idleTimeout }} - tls: - {{- $tls := .apis.http.tls }} - {{- include "plgd-hub.internalCertificateConfig" (list $ $tls $oauthServerCertPath ) | indent 8 }} - clientCertificateRequired: {{ .apis.http.tls.clientCertificateRequired }} + clients: + storage: + cleanUpDeletedTokens: {{ .clients.storage.cleanUpDeletedTokens | quote }} + use: {{ include "plgd-hub.useDatabase" (list $ . .clients.storage.use) | quote }} + mongoDB: + uri: {{ include "plgd-hub.mongoDBUri" (list $ .clients.storage.mongoDB.uri ) | quote }} + database: {{ .clients.storage.mongoDB.database }} + maxPoolSize: {{ .clients.storage.mongoDB.maxPoolSize }} + maxConnIdleTime: {{ .clients.storage.mongoDB.maxConnIdleTime }} + tls: + {{- $mongoDbTls := .clients.storage.mongoDB.tls }} + {{- include "plgd-hub.internalCertificateConfig" (list $ $mongoDbTls $cert ) | indent 10 }} + useSystemCAPool: {{ .clients.storage.mongoDB.tls.useSystemCAPool }} + {{- include "plgd-hub.openTelemetryExporterConfig" (list $ $cert ) | nindent 6 }} oauthSigner: privateKeyFile: {{ include "plgd-hub.m2moauthserver.getPrivateKeyFile" $ }} domain: {{ include "plgd-hub.m2moauthserver.ingressDomain" $ }} @@ -69,7 +102,7 @@ data: jwtPrivateKey: enabled: {{ .jwtPrivateKey.enabled }} authorization: - {{- $authorization := include "plgd-hub.basicAuthorizationConfig" (list $ .jwtPrivateKey.authorization (printf "m2moauthserver.oauthSigner.clients[%v].jwtPrivateKey.authorization" $idx) $oauthServerCertPath) | fromYaml }} + {{- $authorization := include "plgd-hub.basicAuthorizationConfig" (list $ .jwtPrivateKey.authorization (printf "m2moauthserver.oauthSigner.clients[%v].jwtPrivateKey.authorization" $idx) $cert) | fromYaml }} {{- if $authorization.audience }} audience: {{ $authorization.audience | quote }} {{- end }} @@ -80,11 +113,11 @@ data: http: {{- .http | toYaml | nindent 20 }} {{- end }} {{- end }} + tokenTrustVerification: + enabled: false {{- end }} {{- end }} {{- end }} {{- end }} - clients: - {{- include "plgd-hub.openTelemetryExporterConfig" (list $ $oauthServerCertPath) | nindent 6 }} {{- end }} {{- end }} \ No newline at end of file diff --git a/charts/plgd-hub/templates/m2m-oauth-server/deployment.yaml b/charts/plgd-hub/templates/m2m-oauth-server/deployment.yaml index 8189d4da8..ee17f3a27 100644 --- a/charts/plgd-hub/templates/m2m-oauth-server/deployment.yaml +++ b/charts/plgd-hub/templates/m2m-oauth-server/deployment.yaml @@ -56,9 +56,12 @@ spec: - "--config" - {{ printf "%s/%s" .Values.m2moauthserver.config.mountPath .Values.m2moauthserver.config.fileName | quote }} ports: - - name: http + - name: grpc containerPort: {{ .Values.m2moauthserver.port }} protocol: TCP + - name: http + containerPort: {{ .Values.m2moauthserver.httpPort }} + protocol: TCP {{- with .Values.m2moauthserver.livenessProbe }} livenessProbe: {{- toYaml . | nindent 12 }} @@ -78,6 +81,7 @@ spec: - name: service-crt mountPath: {{ $rdServiceCert }} {{- end }} + {{- include "plgd-hub.extraCAPoolMount" (list . .Values.extraCAPool.authorization) | nindent 12 }} {{- include "plgd-hub.extraCAPoolMount" (list . .Values.extraCAPool.internal) | nindent 12 }} {{- with .Values.m2moauthserver.extraVolumeMounts }} {{- toYaml . | nindent 12 }} @@ -87,22 +91,12 @@ spec: mountPath: {{ .Values.m2moauthserver.privateKey.mountPath }} readOnly: true {{- end }} - {{- if include "plgd-hub.m2moauthserver.clientServiceSecretEnabled" $ }} - - name: {{ .Values.m2moauthserver.clientServiceSecret.volume }} - mountPath: {{ .Values.m2moauthserver.clientServiceSecret.mountPath }} - readOnly: true - {{- end }} volumes: {{- if include "plgd-hub.m2moauthserver.privateKeySecretEnabled" $ }} - name: {{ .Values.m2moauthserver.privateKey.volume }} secret: secretName: {{ .Values.m2moauthserver.privateKey.secretName }} {{- end }} - {{- if include "plgd-hub.m2moauthserver.clientServiceSecretEnabled" $ }} - - name: {{ .Values.m2moauthserver.clientServiceSecret.volume }} - secret: - secretName: {{ .Values.m2moauthserver.clientServiceSecret.secretName }} - {{- end }} - name: {{ .Values.m2moauthserver.config.volume }} configMap: name: {{ include "plgd-hub.m2moauthserver.configName" . }} @@ -111,6 +105,7 @@ spec: secret: secretName: {{ include "plgd-hub.m2moauthserver.serviceCertName" . }} {{- end }} + {{- include "plgd-hub.extraCAPoolVolume" (list . .Values.extraCAPool.authorization) | nindent 8 }} {{- include "plgd-hub.extraCAPoolVolume" (list . .Values.extraCAPool.internal) | nindent 8 }} {{- with .Values.m2moauthserver.extraVolumes }} {{- toYaml . | nindent 8 }} diff --git a/charts/plgd-hub/templates/m2m-oauth-server/grpc-ingress.yaml b/charts/plgd-hub/templates/m2m-oauth-server/grpc-ingress.yaml new file mode 100644 index 000000000..c552baa9d --- /dev/null +++ b/charts/plgd-hub/templates/m2m-oauth-server/grpc-ingress.yaml @@ -0,0 +1,40 @@ +{{- if and (include "plgd-hub.m2moauthserver.enabled" .) .Values.m2moauthserver.ingress.grpc.enabled }} +{{- $fullname := include "plgd-hub.m2moauthserver.fullname" . }} +{{- $port := .Values.m2moauthserver.port }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullname }}-grpc + namespace: {{ .Release.Namespace }} + labels: + {{- include "plgd-hub.labels" . | nindent 4 }} + annotations: + {{- if .Values.m2moauthserver.ingress.grpc.annotations }} + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.m2moauthserver.ingress.grpc.annotations "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.m2moauthserver.ingress.grpc.customAnnotations }} + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.m2moauthserver.ingress.grpc.customAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + tls: + - hosts: + - {{ include "plgd-hub.m2moauthserver.ingressDomain" . | quote }} + {{- if $.Values.global.enableWildCartCert }} + secretName: {{ include "plgd-hub.wildCardCertName" . | quote }} + {{- else }} + secretName: {{ include "plgd-hub.m2moauthserver.domainCertName" . | quote }} + {{- end }} + rules: + - host: {{ include "plgd-hub.m2moauthserver.ingressDomain" . | quote }} + http: + paths: + {{- range .Values.m2moauthserver.ingress.grpc.paths }} + - path: {{ . }} + pathType: Prefix + backend: + service: + name: {{ $fullname }}-grpc + port: + number: {{ $port }} + {{- end }} +{{- end }} diff --git a/charts/plgd-hub/templates/m2m-oauth-server/grpc-service.yaml b/charts/plgd-hub/templates/m2m-oauth-server/grpc-service.yaml new file mode 100644 index 000000000..53dcdb956 --- /dev/null +++ b/charts/plgd-hub/templates/m2m-oauth-server/grpc-service.yaml @@ -0,0 +1,25 @@ +{{- if and (include "plgd-hub.m2moauthserver.enabled" .) }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "plgd-hub.m2moauthserver.fullname" . }}-grpc + namespace: {{ .Release.Namespace }} + labels: + {{- include "plgd-hub.labels" . | nindent 4 }} + {{- with .Values.m2moauthserver.service.grpc.labels }} + {{- . | toYaml | nindent 4 }} + {{- end }} + {{- if .Values.m2moauthserver.service.grpc.annotations }} + annotations: + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.m2moauthserver.service.grpc.annotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.m2moauthserver.service.grpc.type | default "ClusterIP" }} + ports: + - port: {{ .Values.m2moauthserver.port }} + targetPort: {{ .Values.m2moauthserver.service.grpc.targetPort }} + protocol: {{ .Values.m2moauthserver.service.grpc.protocol }} + name: {{ .Values.m2moauthserver.service.grpc.name }} + selector: + {{- include "plgd-hub.m2moauthserver.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/charts/plgd-hub/templates/m2m-oauth-server/ingress.yaml b/charts/plgd-hub/templates/m2m-oauth-server/http-ingress.yaml similarity index 72% rename from charts/plgd-hub/templates/m2m-oauth-server/ingress.yaml rename to charts/plgd-hub/templates/m2m-oauth-server/http-ingress.yaml index ad4b8eea1..a062cb3be 100644 --- a/charts/plgd-hub/templates/m2m-oauth-server/ingress.yaml +++ b/charts/plgd-hub/templates/m2m-oauth-server/http-ingress.yaml @@ -1,6 +1,6 @@ -{{- if and (include "plgd-hub.m2moauthserver.enabled" .) .Values.m2moauthserver.ingress.enabled }} +{{- if and (include "plgd-hub.m2moauthserver.enabled" .) .Values.m2moauthserver.ingress.http.enabled }} {{- $fullname := include "plgd-hub.m2moauthserver.fullname" . }} -{{- $port := .Values.m2moauthserver.port }} +{{- $port := .Values.m2moauthserver.httpPort }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: @@ -9,11 +9,11 @@ metadata: labels: {{- include "plgd-hub.labels" . | nindent 4 }} annotations: - {{- if .Values.m2moauthserver.ingress.annotations }} - {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.m2moauthserver.ingress.annotations "context" $ ) | nindent 4 }} + {{- if .Values.m2moauthserver.ingress.http.annotations }} + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.m2moauthserver.ingress.http.annotations "context" $ ) | nindent 4 }} {{- end }} - {{- if .Values.m2moauthserver.ingress.customAnnotations }} - {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.m2moauthserver.ingress.customAnnotations "context" $ ) | nindent 4 }} + {{- if .Values.m2moauthserver.ingress.http.customAnnotations }} + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.m2moauthserver.ingress.http.customAnnotations "context" $ ) | nindent 4 }} {{- end }} spec: tls: @@ -28,7 +28,7 @@ spec: - host: {{ include "plgd-hub.m2moauthserver.ingressDomain" . | quote }} http: paths: - {{- range .Values.m2moauthserver.ingress.paths }} + {{- range .Values.m2moauthserver.ingress.http.paths }} - path: {{ . }} pathType: Prefix backend: diff --git a/charts/plgd-hub/templates/m2m-oauth-server/http-service.yaml b/charts/plgd-hub/templates/m2m-oauth-server/http-service.yaml new file mode 100644 index 000000000..a9010d2b1 --- /dev/null +++ b/charts/plgd-hub/templates/m2m-oauth-server/http-service.yaml @@ -0,0 +1,25 @@ +{{- if include "plgd-hub.m2moauthserver.enabled" . -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "plgd-hub.m2moauthserver.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "plgd-hub.labels" . | nindent 4 }} + {{- with .Values.m2moauthserver.service.http.labels }} + {{- . | toYaml | nindent 4 }} + {{- end }} + {{- if .Values.m2moauthserver.service.http.annotations }} + annotations: + {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.m2moauthserver.service.http.annotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.m2moauthserver.service.http.type | default "ClusterIP" }} + ports: + - port: {{ .Values.m2moauthserver.httpPort }} + targetPort: {{ .Values.m2moauthserver.service.http.targetPort }} + protocol: {{ .Values.m2moauthserver.service.http.protocol }} + name: {{ .Values.m2moauthserver.service.http.name }} + selector: + {{- include "plgd-hub.m2moauthserver.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/charts/plgd-hub/templates/m2m-oauth-server/service-crt.yaml b/charts/plgd-hub/templates/m2m-oauth-server/service-crt.yaml index e5a53c67e..7255493f4 100644 --- a/charts/plgd-hub/templates/m2m-oauth-server/service-crt.yaml +++ b/charts/plgd-hub/templates/m2m-oauth-server/service-crt.yaml @@ -27,8 +27,11 @@ spec: dnsNames: - {{ printf "%s.%s.svc.%s" $serviceDns .Release.Namespace .Values.cluster.dns | quote }} - {{ $serviceDns | quote }} - {{- if .Values.m2moauthserver.service.crt.extraDnsNames }} - {{- toYaml .Values.m2moauthserver.service.crt.extraDnsNames | nindent 4 }} + {{- if .Values.m2moauthserver.service.grpc.crt.extraDnsNames }} + {{- toYaml .Values.m2moauthserver.service.grpc.crt.extraDnsNames | nindent 4 }} + {{- end }} + {{- if .Values.m2moauthserver.service.http.crt.extraDnsNames }} + {{- toYaml .Values.m2moauthserver.service.http.crt.extraDnsNames | nindent 4 }} {{- end }} duration: {{ .Values.certmanager.internal.cert.duration | default .Values.certmanager.default.cert.duration }} renewBefore: {{ .Values.certmanager.internal.cert.renewBefore | default .Values.certmanager.default.cert.renewBefore }} diff --git a/charts/plgd-hub/templates/m2m-oauth-server/service.yaml b/charts/plgd-hub/templates/m2m-oauth-server/service.yaml deleted file mode 100644 index a770904e8..000000000 --- a/charts/plgd-hub/templates/m2m-oauth-server/service.yaml +++ /dev/null @@ -1,25 +0,0 @@ -{{- if include "plgd-hub.m2moauthserver.enabled" . -}} -apiVersion: v1 -kind: Service -metadata: - name: {{ include "plgd-hub.m2moauthserver.fullname" . }} - namespace: {{ .Release.Namespace }} - labels: - {{- include "plgd-hub.labels" . | nindent 4 }} - {{- with .Values.m2moauthserver.service.labels }} - {{- . | toYaml | nindent 4 }} - {{- end }} - {{- if .Values.m2moauthserver.service.annotations }} - annotations: - {{- include "plgd-hub.tplvalues.render" ( dict "value" .Values.m2moauthserver.service.annotations "context" $ ) | nindent 4 }} - {{- end }} -spec: - type: {{ .Values.m2moauthserver.service.type | default "ClusterIP" }} - ports: - - port: {{ .Values.m2moauthserver.port }} - targetPort: {{ .Values.m2moauthserver.service.targetPort }} - protocol: {{ .Values.m2moauthserver.service.protocol }} - name: {{ .Values.m2moauthserver.service.name }} - selector: - {{- include "plgd-hub.m2moauthserver.selectorLabels" . | nindent 4 }} -{{- end }} \ No newline at end of file diff --git a/charts/plgd-hub/templates/snippet-service/config.yaml b/charts/plgd-hub/templates/snippet-service/config.yaml index 8f16a070d..2c790b21f 100644 --- a/charts/plgd-hub/templates/snippet-service/config.yaml +++ b/charts/plgd-hub/templates/snippet-service/config.yaml @@ -30,14 +30,14 @@ data: # 0s - means infinity maxConnectionIdle: {{ .apis.grpc.keepAlive.maxConnectionIdle }} # 0s - means infinity - maxConnectionAge: {{ .apis.grpc.keepAlive.maxConnectionIdle }} + maxConnectionAge: {{ .apis.grpc.keepAlive.maxConnectionAge }} # 0s - means infinity maxConnectionAgeGrace: {{ .apis.grpc.keepAlive.maxConnectionAgeGrace }} - time: {{ .apis.grpc.keepAlive.maxConnectionIdle }} - timeout: {{ .apis.grpc.keepAlive.maxConnectionIdle }} + time: {{ .apis.grpc.keepAlive.time }} + timeout: {{ .apis.grpc.keepAlive.timeout }} tls: {{- $tls := .apis.grpc.tls }} - {{- include "plgd-hub.certificateConfig" (list $ $tls $cert ) | indent 8 }} + {{- include "plgd-hub.internalCertificateConfig" (list $ $tls $cert ) | indent 8 }} clientCertificateRequired: {{ .apis.grpc.tls.clientCertificateRequired }} authorization: {{- $authorization := .apis.grpc.authorization }} @@ -58,7 +58,7 @@ data: bytesLimit: {{ printf "%v" .clients.eventBus.nats.pendingLimits.bytesLimit }} tls: {{- $natsTls := .clients.eventBus.nats.tls }} - {{- include "plgd-hub.certificateConfig" (list $ $natsTls $cert ) | indent 10 }} + {{- include "plgd-hub.internalCertificateConfig" (list $ $natsTls $cert ) | indent 10 }} useSystemCAPool: {{ .clients.eventBus.nats.tls.useSystemCAPool }} {{- if or .clients.eventBus.nats.leadResourceType $.Values.global.nats.leadResourceType }} {{- $leadResourceType := .clients.eventBus.nats.leadResourceType | default $.Values.global.nats.leadResourceType }} @@ -75,7 +75,7 @@ data: maxConnIdleTime: {{ .clients.storage.mongoDB.maxConnIdleTime }} tls: {{- $mongoDbTls := .clients.storage.mongoDB.tls }} - {{- include "plgd-hub.certificateConfig" (list $ $mongoDbTls $cert ) | indent 10 }} + {{- include "plgd-hub.internalCertificateConfig" (list $ $mongoDbTls $cert ) | indent 10 }} useSystemCAPool: {{ .clients.storage.mongoDB.tls.useSystemCAPool }} resourceAggregate: grpc: @@ -89,7 +89,7 @@ data: permitWithoutStream: {{ .clients.resourceAggregate.grpc.keepAlive.permitWithoutStream }} tls: {{- $raClientTls := .clients.resourceAggregate.grpc.tls }} - {{- include "plgd-hub.certificateConfig" (list $ $raClientTls $cert) | indent 10 }} + {{- include "plgd-hub.internalCertificateConfig" (list $ $raClientTls $cert ) | indent 10 }} useSystemCAPool: {{ .clients.resourceAggregate.grpc.tls.useSystemCAPool }} {{- include "plgd-hub.openTelemetryExporterConfig" (list $ $cert ) | nindent 6 }} {{- end }} diff --git a/charts/plgd-hub/templates/snippet-service/deployment.yaml b/charts/plgd-hub/templates/snippet-service/deployment.yaml index 0737a274d..3cbd5e62d 100644 --- a/charts/plgd-hub/templates/snippet-service/deployment.yaml +++ b/charts/plgd-hub/templates/snippet-service/deployment.yaml @@ -87,10 +87,8 @@ spec: - name: service-crt mountPath: {{ $cert }} {{- end }} - {{- if .Values.global.authorizationCAPool }} - - name: {{ .Values.extraAuthorizationCAPool.name }} - mountPath: {{ .Values.extraAuthorizationCAPool.mountPath }} - {{- end }} + {{- include "plgd-hub.extraCAPoolMount" (list . .Values.extraCAPool.authorization) | nindent 12 }} + {{- include "plgd-hub.extraCAPoolMount" (list . .Values.extraCAPool.internal) | nindent 12 }} {{- with .Values.snippetservice.extraVolumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} @@ -106,11 +104,8 @@ spec: secret: secretName: {{ include "plgd-hub.snippetservice.serviceCertName" . }} {{- end }} - {{- if .Values.global.authorizationCAPool }} - - name: {{ .Values.extraAuthorizationCAPool.name }} - secret: - secretName: {{ .Values.extraAuthorizationCAPool.name }} - {{- end }} + {{- include "plgd-hub.extraCAPoolVolume" (list . .Values.extraCAPool.authorization) | nindent 8 }} + {{- include "plgd-hub.extraCAPoolVolume" (list . .Values.extraCAPool.internal) | nindent 8 }} {{- with .Values.snippetservice.extraVolumes }} {{- toYaml . | nindent 8 }} {{- end }} diff --git a/charts/plgd-hub/values.yaml b/charts/plgd-hub/values.yaml index 0985873f5..309d79121 100644 --- a/charts/plgd-hub/values.yaml +++ b/charts/plgd-hub/values.yaml @@ -22,8 +22,6 @@ global: m2mOAuthServer: # -- private key to sign JWT m2m tokens privateKey: "" - # -- service secret to sign JWT m2m tokens for the oauth service client - clientServiceSecret: "" # -- Default OAuth authorization for all services authorization: audience: '{{ include "plgd-hub.globalAudience" . }}' @@ -53,6 +51,9 @@ global: keyFile: certFile: useSystemCAPool: true + tokenTrustVerification: + enabled: true + cacheExpiration: 30s # -- Enable *.{{ global.domain }} for all external domain enableWildCartCert: true # -- Sets cloud to standby mode @@ -2342,9 +2343,9 @@ snippetservice: http: # -- Service type type: ClusterIP - # -- Labels for snippet service + # -- Labels for snippet-service labels: {} - # -- Annotations for snippet service + # -- Annotations for snippet-service annotations: {} # -- Target port targetPort: http @@ -2359,7 +2360,7 @@ snippetservice: rbac: # -- Enable RBAC enabled: false - # -- Name of snippet service SA + # -- Name of snippet-service SA serviceAccountName: snippet-service # -- Template definition for Role/binding etc.. roleBindingDefitionTpl: @@ -2745,20 +2746,39 @@ m2moauthserver: # -- Annotations for m2m-oauth-server pod podAnnotations: {} service: - type: ClusterIP - # -- Labels for m2m-oauth-server service - labels: {} - # -- Annotations for m2m-oauth-server service - annotations: {} - # -- Target port - targetPort: http - # -- Protocol - protocol: TCP - # -- Name - name: http - crt: - # -- Extra DNS names for service certificate - extraDnsNames: [] + grpc: + # -- Service type + type: ClusterIP + # -- Labels for m2m-oauth-server + labels: {} + # -- Annotations for m2m-oauth-server + annotations: {} + # -- Target port + targetPort: grpc + # -- Protocol + protocol: TCP + # -- Name + name: grpc + crt: + # -- Extra DNS names for service certificate + extraDnsNames: [] + http: + # -- Service type + type: ClusterIP + # -- Labels for m2m-oauth-server + labels: {} + # -- Annotations for m2m-oauth-server + annotations: {} + # -- Target port + targetPort: http + # -- Protocol + protocol: TCP + # -- Name + name: http + crt: + # -- Extra DNS names for service certificate + extraDnsNames: [] + # -- RBAC configuration securityContext: {} # -- Image pull secrets imagePullSecrets: {} @@ -2794,20 +2814,40 @@ m2moauthserver: # -- Domain for oauth. Default {{ global.domain }} domain: ingress: - # -- Enable ingress - enabled: true - # -- Pre defined map of Ingress annotation - annotations: - nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" - ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/enable-cors: "true" - cert-manager.io/private-key-rotation-policy: always - # -- Custom map of Ingress annotation - customAnnotations: {} - # -- Ingress path - paths: - - /m2m-oauth-server - allowHeaders: "Authortity,Method,Path,Scheme,Accept,Accept-Encoding,Accept-Language,Content-Type,auth0-client,Origin,Refer,Sec-Fetch-Dest,Sec-Fetch-Mode,Sec-Fetch-Site,Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" + http: + # -- Enable ingress + enabled: true + # -- Override name of host/tls secret. If not specified, it will be generated + secretName: + # -- Pre defined map of Ingress annotation + annotations: + nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" + ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/enable-cors: "true" + cert-manager.io/private-key-rotation-policy: always + # -- Custom map of Ingress annotation + customAnnotations: {} + # -- Ingress path + paths: + - /m2m-oauth-server + allowHeaders: "Authortity,Method,Path,Scheme,Accept,Accept-Encoding,Accept-Language,Content-Type,auth0-client,Origin,Refer,Sec-Fetch-Dest,Sec-Fetch-Mode,Sec-Fetch-Site,Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" + grpc: + # -- Enable ingress + enabled: true + # -- Override name of host/tls secret. If not specified, it will be generated + secretName: + # -- Pre defined map of Ingress annotation + annotations: + nginx.org/grpc-services: '{{ include "plgd-hub.m2moauthserver.fullname" . }}-grpc' + nginx.ingress.kubernetes.io/backend-protocol: "GRPCS" + ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/enable-cors: "true" + cert-manager.io/private-key-rotation-policy: always + # -- Custom map of Ingress annotation + customAnnotations: {} + # -- Paths + paths: + - /m2moauthserver.pb.M2MOAuthService # -- m2m-oauth-server service yaml config section config: # -- Name of configuration file @@ -2818,6 +2858,7 @@ m2moauthserver: mountPath: /config # -- Port for service and POD port: 9100 + httpPort: 9101 privateKey: # -- Set deployment to use secret for private key enabled: false @@ -2829,17 +2870,6 @@ m2moauthserver: volume: private-key # -- Mount path mountPath: /secrets/keys - clientServiceSecret: - # -- Set deployment to use secret for service secret - enabled: false - # -- Name of private key file - fileName: secret.dat - # -- Name of secret - secretName: m2m-service-secret - # -- Volume name - volume: service-secret - # -- Mount path - mountPath: /secrets/clients/service log: # -- Logging enabled from level level: info @@ -2861,11 +2891,56 @@ m2moauthserver: readHeaderTimeout: 4s writeTimeout: 16s idleTimeout: 30s + grpc: + address: "" + sendMsgSize: 4194304 + recvMsgSize: 4194304 + enforcementPolicy: + minTime: 5s + permitWithoutStream: true + keepAlive: + # 0s - means infinity + maxConnectionIdle: 0s + # 0s - means infinity + maxConnectionAge: 0s + # 0s - means infinity + maxConnectionAgeGrace: 0s + time: 2h + timeout: 20s tls: caPool: keyFile: certFile: clientCertificateRequired: false + authorization: + ownerClaim: + authority: + audience: + http: + maxIdleConns: 16 + maxConnsPerHost: 32 + maxIdleConnsPerHost: 16 + idleConnTimeout: "30s" + timeout: "10s" + tls: + caPool: + keyFile: + certFile: + useSystemCAPool: false + clients: + storage: + cleanUpDeletedTokens: "0 * * * *" + use: mongoDB + mongoDB: + uri: + database: m2mOAuthServer + maxPoolSize: 16 + maxConnIdleTime: 4m0s + tls: + caPool: + keyFile: + certFile: + useSystemCAPool: false oauthSigner: privateKeyFile: domain: @@ -2883,10 +2958,3 @@ m2moauthserver: authorization: audience: endpoints: - - id: "service" - secretFile: '{{ include "plgd-hub.m2moauthserver.getClientServiceSecretFile" . }}' - accessTokenLifetime: 0s - allowedGrantTypes: - - client_credentials - allowedAudiences: [] - allowedScopes: [] diff --git a/cloud2cloud-connector/service/deviceSubscription.go b/cloud2cloud-connector/service/deviceSubscription.go index a739fbe5d..f80f8647c 100644 --- a/cloud2cloud-connector/service/deviceSubscription.go +++ b/cloud2cloud-connector/service/deviceSubscription.go @@ -12,7 +12,7 @@ import ( "github.com/plgd-dev/hub/v2/cloud2cloud-connector/events" "github.com/plgd-dev/hub/v2/cloud2cloud-connector/store" "github.com/plgd-dev/hub/v2/pkg/log" - kitHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpUri "github.com/plgd-dev/hub/v2/pkg/net/http/uri" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "go.opentelemetry.io/otel/trace" ) @@ -111,7 +111,7 @@ func (s *SubscriptionManager) handleResourcesPublished(ctx context.Context, d Su Priority: int64(endpoint.Priority), }) } - href := kitHttp.CanonicalHref(trimDeviceIDFromHref(link.DeviceID, link.Href)) + href := pkgHttpUri.CanonicalHref(trimDeviceIDFromHref(link.DeviceID, link.Href)) _, err := s.raClient.PublishResourceLinks(ctx, &commands.PublishResourceLinksRequest{ DeviceId: link.DeviceID, Resources: []*commands.Resource{{ @@ -153,7 +153,7 @@ func (s *SubscriptionManager) handleResourcesUnpublished(ctx context.Context, d var errors *multierror.Error for _, link := range links { link.DeviceID = d.subscription.DeviceID - href := kitHttp.CanonicalHref(trimDeviceIDFromHref(link.DeviceID, link.Href)) + href := pkgHttpUri.CanonicalHref(trimDeviceIDFromHref(link.DeviceID, link.Href)) _, err := s.raClient.UnpublishResourceLinks(ctx, &commands.UnpublishResourceLinksRequest{ DeviceId: link.GetDeviceID(), Hrefs: []string{href}, diff --git a/cloud2cloud-connector/service/publishResource.go b/cloud2cloud-connector/service/publishResource.go index c48677c4a..8646814fe 100644 --- a/cloud2cloud-connector/service/publishResource.go +++ b/cloud2cloud-connector/service/publishResource.go @@ -4,7 +4,7 @@ import ( "context" "github.com/plgd-dev/device/v2/schema" - kitHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpUri "github.com/plgd-dev/hub/v2/pkg/net/http/uri" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" raService "github.com/plgd-dev/hub/v2/resource-aggregate/service" ) @@ -17,7 +17,7 @@ func publishResource(ctx context.Context, raClient raService.ResourceAggregateCl Priority: int64(endpoint.Priority), }) } - href := kitHttp.CanonicalHref(trimDeviceIDFromHref(link.DeviceID, link.Href)) + href := pkgHttpUri.CanonicalHref(trimDeviceIDFromHref(link.DeviceID, link.Href)) _, err := raClient.PublishResourceLinks(ctx, &commands.PublishResourceLinksRequest{ DeviceId: link.DeviceID, Resources: []*commands.Resource{{ diff --git a/cloud2cloud-connector/service/pull.go b/cloud2cloud-connector/service/pull.go index ca97e02e2..336de558d 100644 --- a/cloud2cloud-connector/service/pull.go +++ b/cloud2cloud-connector/service/pull.go @@ -18,7 +18,7 @@ import ( pbIS "github.com/plgd-dev/hub/v2/identity-store/pb" "github.com/plgd-dev/hub/v2/pkg/log" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" - kitHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpUri "github.com/plgd-dev/hub/v2/pkg/net/http/uri" "github.com/plgd-dev/hub/v2/pkg/security/oauth2" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" raService "github.com/plgd-dev/hub/v2/resource-aggregate/service" @@ -300,7 +300,7 @@ func (p *pullDevicesHandler) notifyResourceChanged(ctx context.Context, linkedAc } _, err = p.raClient.NotifyResourceChanged(ctx, &commands.NotifyResourceChangedRequest{ - ResourceId: commands.NewResourceID(deviceID, kitHttp.CanonicalHref(link.Href)), + ResourceId: commands.NewResourceID(deviceID, pkgHttpUri.CanonicalHref(link.Href)), CommandMetadata: &commands.CommandMetadata{ ConnectionId: linkedAccount.ID, Sequence: uint64(time.Now().UnixNano()), diff --git a/cloud2cloud-connector/service/requestHandler.go b/cloud2cloud-connector/service/requestHandler.go index 265cfe217..a94b77d27 100644 --- a/cloud2cloud-connector/service/requestHandler.go +++ b/cloud2cloud-connector/service/requestHandler.go @@ -13,7 +13,8 @@ import ( "github.com/plgd-dev/hub/v2/cloud2cloud-connector/store" "github.com/plgd-dev/hub/v2/cloud2cloud-connector/uri" "github.com/plgd-dev/hub/v2/pkg/log" - kitNetHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpJwt "github.com/plgd-dev/hub/v2/pkg/net/http/jwt" pkgOAuth2 "github.com/plgd-dev/hub/v2/pkg/security/oauth2" "go.opentelemetry.io/otel/trace" ) @@ -78,11 +79,11 @@ func healthCheck(w http.ResponseWriter, _ *http.Request) { } // NewHTTP returns HTTP handler -func NewHTTP(requestHandler *RequestHandler, authInterceptor kitNetHttp.Interceptor, logger log.Logger) (http.Handler, error) { +func NewHTTP(requestHandler *RequestHandler, authInterceptor pkgHttpJwt.Interceptor, logger log.Logger) (http.Handler, error) { r := router.NewRouter() r.StrictSlash(true) - r.Use(kitNetHttp.CreateLoggingMiddleware(kitNetHttp.WithLogger(logger))) - r.Use(kitNetHttp.CreateAuthMiddleware(authInterceptor, func(_ context.Context, w http.ResponseWriter, r *http.Request, err error) { + r.Use(pkgHttp.CreateLoggingMiddleware(pkgHttp.WithLogger(logger))) + r.Use(pkgHttp.CreateAuthMiddleware(authInterceptor, func(_ context.Context, w http.ResponseWriter, r *http.Request, err error) { logAndWriteErrorResponse(fmt.Errorf("cannot process request on %v: %w", r.RequestURI, err), http.StatusUnauthorized, w) })) diff --git a/cloud2cloud-connector/service/resourceSubscription.go b/cloud2cloud-connector/service/resourceSubscription.go index 732a9c51f..976ccd8e8 100644 --- a/cloud2cloud-connector/service/resourceSubscription.go +++ b/cloud2cloud-connector/service/resourceSubscription.go @@ -10,7 +10,7 @@ import ( "github.com/plgd-dev/go-coap/v3/pkg/cache" "github.com/plgd-dev/hub/v2/cloud2cloud-connector/events" "github.com/plgd-dev/hub/v2/cloud2cloud-connector/store" - kitHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpUri "github.com/plgd-dev/hub/v2/pkg/net/http/uri" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "go.opentelemetry.io/otel/trace" ) @@ -88,7 +88,7 @@ func cancelResourceSubscription(ctx context.Context, traceProvider trace.TracerP func (s *SubscriptionManager) handleResourceChangedEvent(ctx context.Context, subscriptionData SubscriptionData, header events.EventHeader, body []byte) error { coapContentFormat := stringToSupportedMediaType(header.ContentType) _, err := s.raClient.NotifyResourceChanged(ctx, &commands.NotifyResourceChangedRequest{ - ResourceId: commands.NewResourceID(subscriptionData.subscription.DeviceID, kitHttp.CanonicalHref(subscriptionData.subscription.Href)), + ResourceId: commands.NewResourceID(subscriptionData.subscription.DeviceID, pkgHttpUri.CanonicalHref(subscriptionData.subscription.Href)), CommandMetadata: &commands.CommandMetadata{ ConnectionId: subscriptionData.linkedAccount.ID + "." + subscriptionData.subscription.ID, Sequence: header.SequenceNumber, diff --git a/cloud2cloud-connector/service/service.go b/cloud2cloud-connector/service/service.go index ee1cf1e4c..b7c1d5078 100644 --- a/cloud2cloud-connector/service/service.go +++ b/cloud2cloud-connector/service/service.go @@ -18,7 +18,8 @@ import ( pkgMongo "github.com/plgd-dev/hub/v2/pkg/mongodb" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" grpcClient "github.com/plgd-dev/hub/v2/pkg/net/grpc/client" - kitNetHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpJwt "github.com/plgd-dev/hub/v2/pkg/net/http/jwt" "github.com/plgd-dev/hub/v2/pkg/net/listener" otelClient "github.com/plgd-dev/hub/v2/pkg/opentelemetry/collector/client" cmClient "github.com/plgd-dev/hub/v2/pkg/security/certManager/client" @@ -53,10 +54,10 @@ func toValidator(c oauth2.Config) validator.Config { const serviceName = "cloud2cloud-connector" -func newAuthInterceptor(validator *validator.Validator) kitNetHttp.Interceptor { - authRules := kitNetHttp.NewDefaultAuthorizationRules(uri.API) +func newAuthInterceptor(validator *validator.Validator) pkgHttpJwt.Interceptor { + authRules := pkgHttp.NewDefaultAuthorizationRules(uri.API) - whiteList := []kitNetHttp.RequestMatcher{ + whiteList := []pkgHttpJwt.RequestMatcher{ { Method: http.MethodGet, URI: regexp.MustCompile(regexp.QuoteMeta(uri.OAuthCallback)), @@ -70,7 +71,7 @@ func newAuthInterceptor(validator *validator.Validator) kitNetHttp.Interceptor { URI: regexp.MustCompile(regexp.QuoteMeta(uri.Events)), }, } - auth := kitNetHttp.NewInterceptorWithValidator(validator, authRules, whiteList...) + auth := pkgHttpJwt.NewInterceptorWithValidator(validator, authRules, whiteList...) return auth } @@ -267,7 +268,7 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg } httpServer := http.Server{ - Handler: kitNetHttp.OpenTelemetryNewHandler(httpHandler, serviceName, tracerProvider), + Handler: pkgHttp.OpenTelemetryNewHandler(httpHandler, serviceName, tracerProvider), ReadTimeout: config.APIs.HTTP.Server.ReadTimeout, ReadHeaderTimeout: config.APIs.HTTP.Server.ReadHeaderTimeout, WriteTimeout: config.APIs.HTTP.Server.WriteTimeout, diff --git a/cloud2cloud-connector/service/subscriptions.go b/cloud2cloud-connector/service/subscriptions.go index d95a30792..f7ad167d2 100644 --- a/cloud2cloud-connector/service/subscriptions.go +++ b/cloud2cloud-connector/service/subscriptions.go @@ -14,7 +14,7 @@ import ( pbIS "github.com/plgd-dev/hub/v2/identity-store/pb" "github.com/plgd-dev/hub/v2/pkg/log" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" - kitHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpUri "github.com/plgd-dev/hub/v2/pkg/net/http/uri" "github.com/plgd-dev/hub/v2/pkg/security/oauth2" raService "github.com/plgd-dev/hub/v2/resource-aggregate/service" "github.com/plgd-dev/kit/v2/codec/json" @@ -94,7 +94,7 @@ func subscribe(ctx context.Context, tracerProvider trace.TracerProvider, href, c r, w := io.Pipe() - req, err := http.NewRequestWithContext(ctx, http.MethodPost, linkedCloud.Endpoint.URL+kitHttp.CanonicalHref(href), r) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, linkedCloud.Endpoint.URL+pkgHttpUri.CanonicalHref(href), r) if err != nil { return resp, fmt.Errorf("cannot create post request: %w", err) } @@ -138,7 +138,7 @@ func subscribe(ctx context.Context, tracerProvider trace.TracerProvider, href, c func cancelSubscription(ctx context.Context, tracerProvider trace.TracerProvider, href string, linkedAccount store.LinkedAccount, linkedCloud store.LinkedCloud) error { client := linkedCloud.GetHTTPClient(tracerProvider) defer client.CloseIdleConnections() - req, err := http.NewRequestWithContext(ctx, http.MethodDelete, linkedCloud.Endpoint.URL+kitHttp.CanonicalHref(href), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, linkedCloud.Endpoint.URL+pkgHttpUri.CanonicalHref(href), nil) if err != nil { return fmt.Errorf("cannot create delete request: %w", err) } diff --git a/cloud2cloud-connector/service/updateResource.go b/cloud2cloud-connector/service/updateResource.go index fb53a7e78..4f6aa010f 100644 --- a/cloud2cloud-connector/service/updateResource.go +++ b/cloud2cloud-connector/service/updateResource.go @@ -12,7 +12,7 @@ import ( "github.com/plgd-dev/hub/v2/cloud2cloud-connector/store" "github.com/plgd-dev/hub/v2/pkg/log" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" - kitHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpUri "github.com/plgd-dev/hub/v2/pkg/net/http/uri" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" raEvents "github.com/plgd-dev/hub/v2/resource-aggregate/events" raService "github.com/plgd-dev/hub/v2/resource-aggregate/service" @@ -20,7 +20,7 @@ import ( ) func makeHTTPEndpoint(url, deviceID, href string) string { - return url + kitHttp.CanonicalHref("devices/"+deviceID+"/"+href) + return url + pkgHttpUri.CanonicalHref("devices/"+deviceID+"/"+href) } func updateDeviceResource(ctx context.Context, tracerProvider trace.TracerProvider, deviceID, href, contentType string, content []byte, linkedAccount store.LinkedAccount, linkedCloud store.LinkedCloud) (string, []byte, commands.Status, error) { diff --git a/cloud2cloud-gateway/config.yaml b/cloud2cloud-gateway/config.yaml index ad202cd39..c95ad7d26 100644 --- a/cloud2cloud-gateway/config.yaml +++ b/cloud2cloud-gateway/config.yaml @@ -32,6 +32,9 @@ apis: keyFile: "/secrets/private/cert.key" certFile: "/secrets/public/cert.crt" useSystemCAPool: false + tokenTrustVerification: + enabled: true + cacheExpiration: 30s clients: eventBus: nats: diff --git a/cloud2cloud-gateway/service/httpApi.go b/cloud2cloud-gateway/service/httpApi.go index 9da7ef2e7..4ffe5b209 100644 --- a/cloud2cloud-gateway/service/httpApi.go +++ b/cloud2cloud-gateway/service/httpApi.go @@ -13,7 +13,9 @@ import ( "github.com/plgd-dev/hub/v2/cloud2cloud-gateway/uri" pbGRPC "github.com/plgd-dev/hub/v2/grpc-gateway/pb" "github.com/plgd-dev/hub/v2/pkg/log" - kitNetHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpJwt "github.com/plgd-dev/hub/v2/pkg/net/http/jwt" + pkgHttpUri "github.com/plgd-dev/hub/v2/pkg/net/http/uri" raClient "github.com/plgd-dev/hub/v2/resource-aggregate/client" "github.com/plgd-dev/kit/v2/codec/cbor" "github.com/plgd-dev/kit/v2/codec/json" @@ -45,7 +47,7 @@ type RequestHandler struct { func logAndWriteErrorResponse(err error, statusCode int, w http.ResponseWriter) { log.Errorf("%v", err) w.Header().Set(events.ContentTypeKey, "text/plain") - w.WriteHeader(kitNetHttp.ErrToStatusWithDef(err, statusCode)) + w.WriteHeader(pkgHttp.ErrToStatusWithDef(err, statusCode)) if _, err2 := w.Write([]byte(err.Error())); err2 != nil { log.Errorf("failed to write error response body: %w", err2) } @@ -128,7 +130,7 @@ func makeHref(path []string) string { } func splitDevicePath(requestURI string) []string { - p := kitNetHttp.CanonicalHref(requestURI) + p := pkgHttpUri.CanonicalHref(requestURI) p = strings.TrimPrefix(p, uri.Devices) // remove core prefix p = strings.TrimLeft(p, "/") return strings.Split(p, "/") @@ -168,11 +170,11 @@ func resourceMatcher(r *http.Request, rm *router.RouteMatch) bool { } // NewHTTP returns HTTP handler -func NewHTTP(requestHandler *RequestHandler, authInterceptor kitNetHttp.Interceptor, logger log.Logger) http.Handler { +func NewHTTP(requestHandler *RequestHandler, authInterceptor pkgHttpJwt.Interceptor, logger log.Logger) http.Handler { r := router.NewRouter() r.StrictSlash(true) - r.Use(kitNetHttp.CreateLoggingMiddleware(kitNetHttp.WithLogger(logger))) - r.Use(kitNetHttp.CreateAuthMiddleware(authInterceptor, func(_ context.Context, w http.ResponseWriter, r *http.Request, err error) { + r.Use(pkgHttp.CreateLoggingMiddleware(pkgHttp.WithLogger(logger))) + r.Use(pkgHttp.CreateAuthMiddleware(authInterceptor, func(_ context.Context, w http.ResponseWriter, r *http.Request, err error) { logAndWriteErrorResponse(fmt.Errorf("cannot process request on %v: %w", r.RequestURI, err), http.StatusUnauthorized, w) })) diff --git a/cloud2cloud-gateway/service/service.go b/cloud2cloud-gateway/service/service.go index 536f7d899..44f38897b 100644 --- a/cloud2cloud-gateway/service/service.go +++ b/cloud2cloud-gateway/service/service.go @@ -14,7 +14,8 @@ import ( "github.com/plgd-dev/hub/v2/pkg/log" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" grpcClient "github.com/plgd-dev/hub/v2/pkg/net/grpc/client" - kitNetHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpJwt "github.com/plgd-dev/hub/v2/pkg/net/http/jwt" "github.com/plgd-dev/hub/v2/pkg/net/listener" otelClient "github.com/plgd-dev/hub/v2/pkg/opentelemetry/collector/client" cmClient "github.com/plgd-dev/hub/v2/pkg/security/certManager/client" @@ -38,7 +39,7 @@ type Server struct { } // https://openconnectivity.org/draftspecs/Gaborone/OCF_Cloud_API_for_Cloud_Services.pdf -var authRules = map[string][]kitNetHttp.AuthArgs{ +var authRules = map[string][]pkgHttpJwt.AuthArgs{ http.MethodGet: { { URI: regexp.MustCompile(`[\/]+api[\/]+v1[\/]+devices[\/]*$`), @@ -249,7 +250,7 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg return nil, fmt.Errorf("cannot create validator: %w", err) } listener.AddCloseFunc(validator.Close) - auth := kitNetHttp.NewInterceptorWithValidator(validator, authRules) + auth := pkgHttpJwt.NewInterceptorWithValidator(validator, authRules) gwClient, closeGwClient, err := newGrpcGatewayClient(config.Clients.GrpcGateway, fileWatcher, logger, tracerProvider) if err != nil { @@ -298,7 +299,7 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg requestHandler := NewRequestHandler(gwClient, raClient, subMgr, emitEvent) httpServer := http.Server{ - Handler: kitNetHttp.OpenTelemetryNewHandler(NewHTTP(requestHandler, auth, logger), serviceName, tracerProvider), + Handler: pkgHttp.OpenTelemetryNewHandler(NewHTTP(requestHandler, auth, logger), serviceName, tracerProvider), ReadTimeout: config.APIs.HTTP.Server.ReadTimeout, ReadHeaderTimeout: config.APIs.HTTP.Server.ReadHeaderTimeout, WriteTimeout: config.APIs.HTTP.Server.WriteTimeout, diff --git a/cloud2cloud-gateway/service/subscribeToResource.go b/cloud2cloud-gateway/service/subscribeToResource.go index d91050545..526ce6a8f 100644 --- a/cloud2cloud-gateway/service/subscribeToResource.go +++ b/cloud2cloud-gateway/service/subscribeToResource.go @@ -28,7 +28,7 @@ func (rh *RequestHandler) makeSubscription(r *http.Request, typ store.Type, vali return res, http.StatusBadRequest, fmt.Errorf("invalid eventsurl(%w)", err) } - token, err := pkgHttp.ParseToken(r.Header.Get("Authorization")) + token, err := pkgHttp.GetToken(r.Header.Get("Authorization")) if err != nil { return res, http.StatusUnauthorized, fmt.Errorf("invalid accessToken(%w)", err) } diff --git a/cloud2cloud-gateway/service/subscribeToResource_test.go b/cloud2cloud-gateway/service/subscribeToResource_test.go index b4cf32957..119b00754 100644 --- a/cloud2cloud-gateway/service/subscribeToResource_test.go +++ b/cloud2cloud-gateway/service/subscribeToResource_test.go @@ -71,7 +71,7 @@ func TestRequestHandlerSubscribeToResourceTokenTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - services := service.SetUpServicesOAuth | service.SetUpServicesId | service.SetUpServicesCertificateAuthority | + services := service.SetUpServicesOAuth | service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesId | service.SetUpServicesCertificateAuthority | service.SetUpServicesResourceAggregate | service.SetUpServicesResourceDirectory | service.SetUpServicesGrpcGateway | service.SetUpServicesCoapGateway tearDown := service.SetUpServices(ctx, t, services) diff --git a/coap-gateway/config.yaml b/coap-gateway/config.yaml index 14b2c7b76..c0da0ed20 100644 --- a/coap-gateway/config.yaml +++ b/coap-gateway/config.yaml @@ -74,6 +74,9 @@ apis: keyFile: "/secrets/private/cert.key" certFile: "/secrets/public/cert.crt" useSystemCAPool: false + tokenTrustVerification: + enabled: true + cacheExpiration: 30s clients: eventBus: nats: diff --git a/coap-gateway/service/devicesStatusUpdater_test.go b/coap-gateway/service/devicesStatusUpdater_test.go index 02eba2127..3559eadab 100644 --- a/coap-gateway/service/devicesStatusUpdater_test.go +++ b/coap-gateway/service/devicesStatusUpdater_test.go @@ -21,6 +21,7 @@ import ( "github.com/plgd-dev/hub/v2/test/device" oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" testService "github.com/plgd-dev/hub/v2/test/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,7 +31,8 @@ import ( func onboardDeviceAndGetDevice(ctx context.Context, t *testing.T, device device.Device, oauthCfg oauthService.Config, coapCfg coapService.Config, wait time.Duration) (*pb.Device, time.Time /*startOnboard*/, time.Duration /*delta*/) { oauthShutdown := oauthTest.New(t, oauthCfg) - servicesTeardown := testService.SetUpServices(context.Background(), t, testService.SetUpServicesCertificateAuthority|testService.SetUpServicesId|testService.SetUpServicesResourceAggregate|testService.SetUpServicesResourceDirectory|testService.SetUpServicesCoapGateway|testService.SetUpServicesGrpcGateway, testService.WithCOAPGWConfig(coapCfg)) + servicesTeardown := testService.SetUpServices(context.Background(), t, service.SetUpServicesMachine2MachineOAuth|testService.SetUpServicesCertificateAuthority|testService.SetUpServicesId|testService.SetUpServicesResourceAggregate| + testService.SetUpServicesResourceDirectory|testService.SetUpServicesCoapGateway|testService.SetUpServicesGrpcGateway, testService.WithCOAPGWConfig(coapCfg)) ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ diff --git a/coap-gateway/service/mem_test.go b/coap-gateway/service/mem_test.go index 94a502d2b..6b667001a 100644 --- a/coap-gateway/service/mem_test.go +++ b/coap-gateway/service/mem_test.go @@ -179,7 +179,7 @@ func testDevices(t *testing.T, numDevices, numResources, expRSSInMB int, resourc require.NoError(t, err) rdConfig := rdTest.MakeConfig(&testingT{}) rdConfig.Clients.Eventstore.ProjectionCacheExpiration = time.Second * 2 - const services = service.SetUpServicesOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | + const services = service.SetUpServicesOAuth | service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | service.SetUpServicesGrpcGateway | service.SetUpServicesResourceAggregate rdCfgFile, err := os.CreateTemp("", "rd") require.NoError(t, err) diff --git a/coap-gateway/service/observation/deviceObserver_test.go b/coap-gateway/service/observation/deviceObserver_test.go index 54fd8882f..45563cc49 100644 --- a/coap-gateway/service/observation/deviceObserver_test.go +++ b/coap-gateway/service/observation/deviceObserver_test.go @@ -376,7 +376,7 @@ type ( func runTestDeviceObserverRegister(ctx context.Context, t *testing.T, deviceID string, expectedObserved, expectedRetrieved strings.Set, verifyHandler verifyHandlerFn, prepareHub, postHub actioneHubFn, requireBatchObserveEnabled bool) { // TODO: add test with expectedRetrieved - const services = service.SetUpServicesOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | + const services = service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | service.SetUpServicesGrpcGateway | service.SetUpServicesResourceAggregate isConfig := isTest.MakeConfig(t) diff --git a/coap-gateway/service/observation/observation_test.go b/coap-gateway/service/observation/observation_test.go index f8f88c16d..b32936596 100644 --- a/coap-gateway/service/observation/observation_test.go +++ b/coap-gateway/service/observation/observation_test.go @@ -44,7 +44,7 @@ func TestIsResourceObservableWithInterface(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - const services = service.SetUpServicesOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | + const services = service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | service.SetUpServicesGrpcGateway tearDown := service.SetUpServices(ctx, t, services) defer tearDown() diff --git a/coap-gateway/service/reconnect_test.go b/coap-gateway/service/reconnect_test.go index da224abe4..9e03ac48f 100644 --- a/coap-gateway/service/reconnect_test.go +++ b/coap-gateway/service/reconnect_test.go @@ -15,11 +15,9 @@ import ( coapCodes "github.com/plgd-dev/go-coap/v3/message/codes" coapgwTest "github.com/plgd-dev/hub/v2/coap-gateway/test" "github.com/plgd-dev/hub/v2/coap-gateway/uri" - idTest "github.com/plgd-dev/hub/v2/identity-store/test" - raTest "github.com/plgd-dev/hub/v2/resource-aggregate/test" rdTest "github.com/plgd-dev/hub/v2/resource-directory/test" test "github.com/plgd-dev/hub/v2/test" - oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" testService "github.com/plgd-dev/hub/v2/test/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -59,19 +57,12 @@ func TestReconnectNATSAndGrpcGateway(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), TestExchangeTimeout) defer cancel() testService.ClearDB(ctx, t) - oauthShutdown := oauthTest.SetUp(t) - auShutdown := idTest.SetUp(t) - raShutdown := raTest.SetUp(t) - rdShutdown := rdTest.SetUp(t) coapgwCfg := coapgwTest.MakeConfig(t) coapgwCfg.Log.DumpBody = true - gwShutdown := coapgwTest.New(t, coapgwCfg) - defer func() { - gwShutdown() - raShutdown() - auShutdown() - oauthShutdown() - }() + teardown := testService.SetUpServices(ctx, t, testService.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth|service.SetUpServicesMachine2MachineOAuth|testService.SetUpServicesCertificateAuthority|testService.SetUpServicesId|testService.SetUpServicesResourceAggregate| + testService.SetUpServicesCoapGateway, testService.WithCOAPGWConfig(coapgwCfg)) + defer teardown() + rdShutdown := rdTest.SetUp(t) co := testCoapDial(t, "", true, true, time.Now().Add(time.Minute)) if co == nil { diff --git a/coap-gateway/service/refreshToken_test.go b/coap-gateway/service/refreshToken_test.go index 351e60d1d..4af76770b 100644 --- a/coap-gateway/service/refreshToken_test.go +++ b/coap-gateway/service/refreshToken_test.go @@ -16,6 +16,7 @@ import ( "github.com/plgd-dev/hub/v2/pkg/net/listener" "github.com/plgd-dev/hub/v2/test/config" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" testService "github.com/plgd-dev/hub/v2/test/service" "github.com/stretchr/testify/require" ) @@ -96,7 +97,7 @@ func TestRefreshTokenWithOAuthNotWorking(t *testing.T) { oauthShutdown := oauthTest.New(t, cfg) coapgwCfg := coapgwTest.MakeConfig(t) coapgwCfg.APIs.COAP.Authorization.Providers[0].HTTP.Timeout = time.Second - shutdown := testService.SetUpServices(ctx, t, testService.SetUpServicesId|testService.SetUpServicesCoapGateway|testService.SetUpServicesResourceAggregate|testService.SetUpServicesResourceDirectory, testService.WithCOAPGWConfig(coapgwCfg)) + shutdown := testService.SetUpServices(ctx, t, service.SetUpServicesMachine2MachineOAuth|testService.SetUpServicesId|testService.SetUpServicesCoapGateway|testService.SetUpServicesResourceAggregate|testService.SetUpServicesResourceDirectory, testService.WithCOAPGWConfig(coapgwCfg)) defer shutdown() co := testCoapDial(t, "", true, true, time.Now().Add(time.Minute)) diff --git a/coap-gateway/service/service_test.go b/coap-gateway/service/service_test.go index c705725fa..42cc309ec 100644 --- a/coap-gateway/service/service_test.go +++ b/coap-gateway/service/service_test.go @@ -73,7 +73,7 @@ func TestServiceConfigWithDataScheme(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - const services = service.SetUpServicesOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | + const services = service.SetUpServicesOAuth | service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | service.SetUpServicesGrpcGateway | service.SetUpServicesResourceAggregate tearDown := service.SetUpServices(ctx, t, services) defer tearDown() @@ -87,7 +87,7 @@ func TestShutdownServiceWithDeviceIssue627(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - const services = service.SetUpServicesOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | + const services = service.SetUpServicesOAuth | service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | service.SetUpServicesGrpcGateway | service.SetUpServicesResourceAggregate tearDown := service.SetUpServices(ctx, t, services) defer tearDown() diff --git a/coap-gateway/service/utils_test.go b/coap-gateway/service/utils_test.go index eb7aad785..2a11aa712 100644 --- a/coap-gateway/service/utils_test.go +++ b/coap-gateway/service/utils_test.go @@ -479,7 +479,8 @@ func setUp(t *testing.T, coapgwCfgs ...service.Config) func() { if len(coapgwCfgs) > 0 { coapgwCfg = coapgwCfgs[0] } - return testService.SetUpServices(context.Background(), t, testService.SetUpServicesCertificateAuthority|testService.SetUpServicesOAuth|testService.SetUpServicesId|testService.SetUpServicesResourceAggregate|testService.SetUpServicesResourceDirectory|testService.SetUpServicesCoapGateway|testService.SetUpServicesGrpcGateway, testService.WithCOAPGWConfig(coapgwCfg)) + return testService.SetUpServices(context.Background(), t, testService.SetUpServicesMachine2MachineOAuth|testService.SetUpServicesCertificateAuthority|testService.SetUpServicesOAuth| + testService.SetUpServicesId|testService.SetUpServicesResourceAggregate|testService.SetUpServicesResourceDirectory|testService.SetUpServicesCoapGateway|testService.SetUpServicesGrpcGateway, testService.WithCOAPGWConfig(coapgwCfg)) } var ( diff --git a/grpc-gateway/config.yaml b/grpc-gateway/config.yaml index cfa79e2dc..4dc98e5be 100644 --- a/grpc-gateway/config.yaml +++ b/grpc-gateway/config.yaml @@ -45,6 +45,9 @@ apis: keyFile: "/secrets/private/cert.key" certFile: "/secrets/public/cert.crt" useSystemCAPool: false + tokenTrustVerification: + enabled: true + cacheExpiration: 30s clients: identityStore: grpc: diff --git a/grpc-gateway/pb/devices.go b/grpc-gateway/pb/devices.go index 586063f1a..478b1f603 100644 --- a/grpc-gateway/pb/devices.go +++ b/grpc-gateway/pb/devices.go @@ -2,6 +2,7 @@ package pb import ( "encoding/base64" + "slices" "strings" commands "github.com/plgd-dev/hub/v2/resource-aggregate/commands" @@ -94,3 +95,10 @@ func (f *ResourceIdFilter) ToString() string { } return sb.String() } + +func (r *Resource) Clone() *Resource { + return &Resource{ + Types: slices.Clone(r.GetTypes()), + Data: r.GetData().Clone(), + } +} diff --git a/grpc-gateway/pb/hubConfiguration.pb.go b/grpc-gateway/pb/hubConfiguration.pb.go index 497f005cc..716c681e5 100644 --- a/grpc-gateway/pb/hubConfiguration.pb.go +++ b/grpc-gateway/pb/hubConfiguration.pb.go @@ -66,7 +66,7 @@ type OAuthClient struct { ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty" yaml:"clientID"` // @gotags: yaml:"clientID" Audience string `protobuf:"bytes,2,opt,name=audience,proto3" json:"audience,omitempty"` Scopes []string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` - ProviderName string `protobuf:"bytes,4,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty" yaml:"providerName"` // @gotags: yaml:"providerName" + ProviderName string `protobuf:"bytes,4,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty" yaml:"providerName"` // @gotags: yaml:"providerName" ClientAssertionType string `protobuf:"bytes,5,opt,name=client_assertion_type,json=clientAssertionType,proto3" json:"client_assertion_type,omitempty" yaml:"clientAssertionType"` // @gotags: yaml:"clientAssertionType" Authority string `protobuf:"bytes,6,opt,name=authority,proto3" json:"authority,omitempty"` GrantType string `protobuf:"bytes,7,opt,name=grant_type,json=grantType,proto3" json:"grant_type,omitempty" yaml:"grantType"` // @gotags: yaml:"grantType" diff --git a/grpc-gateway/service/getResources_test.go b/grpc-gateway/service/getResources_test.go index e16e45e11..df2950b0b 100644 --- a/grpc-gateway/service/getResources_test.go +++ b/grpc-gateway/service/getResources_test.go @@ -5,12 +5,16 @@ import ( "crypto/tls" "errors" "io" + "sync" "testing" "time" "github.com/plgd-dev/device/v2/test/resource/types" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" - kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + grpcgwTest "github.com/plgd-dev/hub/v2/grpc-gateway/test" + m2mOauthTest "github.com/plgd-dev/hub/v2/m2m-oauth-server/test" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgJwt "github.com/plgd-dev/hub/v2/pkg/security/jwt" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" "github.com/plgd-dev/hub/v2/test" @@ -19,11 +23,31 @@ import ( oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) +func getResources(ctx context.Context, c pb.GrpcGatewayClient, req *pb.GetResourcesRequest) ([]*pb.Resource, error) { + client, err := c.GetResources(ctx, req) + if err != nil { + return nil, err + } + values := make([]*pb.Resource, 0, 1) + for { + value, err := client.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, err + } + values = append(values, value) + } + return values, nil +} + func TestRequestHandlerGetResources(t *testing.T) { deviceID := test.MustFindDeviceByName(test.TestDeviceName) @@ -32,7 +56,7 @@ func TestRequestHandlerGetResources(t *testing.T) { tearDown := service.SetUp(ctx, t) defer tearDown() - ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + ctx = pkgGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ RootCAs: test.GetRootCertificatePool(t), @@ -180,17 +204,8 @@ func TestRequestHandlerGetResources(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - client, err := c.GetResources(ctx, tt.args.req) + values, err := getResources(ctx, c, tt.args.req) require.NoError(t, err) - values := make([]*pb.Resource, 0, 1) - for { - value, err := client.Recv() - if errors.Is(err, io.EOF) { - break - } - require.NoError(t, err) - values = append(values, value) - } if tt.cmpFn != nil { tt.cmpFn(t, tt.want, values) return @@ -199,3 +214,108 @@ func TestRequestHandlerGetResources(t *testing.T) { }) } } + +func TestRequestHandlerGetResourcesWithM2MTokenVerification(t *testing.T) { + deviceID := test.MustFindDeviceByName(test.TestDeviceName) + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + grpcCfg := grpcgwTest.MakeConfig(t) + grpcCfg.APIs.GRPC.Authorization.TokenVerification.CacheExpiration = time.Second * 2 + tearDown := service.SetUp(ctx, t, service.WithGRPCGWConfig(grpcCfg)) + defer tearDown() + validTokenStr := oauthTest.GetDefaultAccessToken(t) + ctxWithToken := pkgGrpc.CtxWithToken(ctx, validTokenStr) + + conn, err := grpc.NewClient(config.GRPC_GW_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := pb.NewGrpcGatewayClient(conn) + + _, shutdownDevSim := test.OnboardDevSim(ctxWithToken, t, c, deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, test.GetAllBackendResourceLinks()) + defer shutdownDevSim() + + req := &pb.GetResourcesRequest{ResourceIdFilter: []*pb.ResourceIdFilter{{ResourceId: commands.NewResourceID(deviceID, test.TestResourceLightInstanceHref("1"))}}} + exp := &pb.Resource{ + Types: []string{types.CORE_LIGHT}, + Data: pbTest.MakeResourceChanged(t, deviceID, test.TestResourceLightInstanceHref("1"), test.TestResourceLightInstanceResourceTypes, "", + map[string]interface{}{ + "state": false, + "power": uint64(0), + "name": "Light", + }), + } + + tokenStr := m2mOauthTest.GetDefaultAccessToken(t) + values, err := getResources(pkgGrpc.CtxWithToken(ctx, tokenStr), c, req) + require.NoError(t, err) + require.NotEmpty(t, values) + pbTest.CmpResourceValues(t, []*pb.Resource{exp.Clone()}, values) + + // invalid token + _, err = getResources(pkgGrpc.CtxWithToken(ctx, "invalid"), c, req) + require.Error(t, err) + + // whitelisted tokens expire + time.Sleep(grpcCfg.APIs.GRPC.Authorization.TokenVerification.CacheExpiration + time.Second) + + // blacklist the token + token, err := pkgJwt.ParseToken(tokenStr) + require.NoError(t, err) + tokenID, err := token.GetID() + require.NoError(t, err) + m2mOauthTest.BlacklistTokens(ctx, t, []string{tokenID}, validTokenStr) + // request should fail + _, err = getResources(pkgGrpc.CtxWithToken(ctx, tokenStr), c, req) + require.ErrorContains(t, err, pkgJwt.ErrBlackListedToken.Error()) + + // repeated requests should still fail, but use the cache + _, err = getResources(pkgGrpc.CtxWithToken(ctx, tokenStr), c, req) + require.ErrorContains(t, err, pkgJwt.ErrBlackListedToken.Error()) + + // non-blacklisted tokens should still work + values, err = getResources(pkgGrpc.CtxWithToken(ctx, validTokenStr), c, req) + require.NoError(t, err) + require.NotEmpty(t, values) + pbTest.CmpResourceValues(t, []*pb.Resource{exp.Clone()}, values) + + // parallel whitelisted requests -> cache should be used, only a single request should be made + var wg sync.WaitGroup + tokenStr2 := m2mOauthTest.GetDefaultAccessToken(t) + for range 5 { + wg.Add(1) + go func() { + defer wg.Done() + values2, err2 := getResources(pkgGrpc.CtxWithToken(ctx, tokenStr2), c, req) + assert.NoError(t, err2) + assert.NotEmpty(t, values2) + pbTest.CmpResourceValues(t, []*pb.Resource{exp.Clone()}, values2) + }() + } + wg.Wait() + + // wait for expiration + time.Sleep(grpcCfg.APIs.GRPC.Authorization.TokenVerification.CacheExpiration) + + // blacklist the token + tokenStr3 := m2mOauthTest.GetDefaultAccessToken(t) + token, err = pkgJwt.ParseToken(tokenStr3) + require.NoError(t, err) + tokenID, err = token.GetID() + require.NoError(t, err) + m2mOauthTest.BlacklistTokens(ctx, t, []string{tokenID}, validTokenStr) + // parallel blacklisted requests -> cache should be used, only a single request should be made + for range 5 { + wg.Add(1) + go func() { + defer wg.Done() + _, err2 := getResources(pkgGrpc.CtxWithToken(ctx, tokenStr3), c, req) + assert.ErrorContains(t, err2, pkgJwt.ErrBlackListedToken.Error()) + }() + } + wg.Wait() +} diff --git a/grpc-gateway/service/subscribeToEvents_test.go b/grpc-gateway/service/subscribeToEvents_test.go index 4c006c914..8233b93fc 100644 --- a/grpc-gateway/service/subscribeToEvents_test.go +++ b/grpc-gateway/service/subscribeToEvents_test.go @@ -13,14 +13,12 @@ import ( "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/schema/platform" "github.com/plgd-dev/go-coap/v3/message" - caService "github.com/plgd-dev/hub/v2/certificate-authority/test" coapgwTest "github.com/plgd-dev/hub/v2/coap-gateway/test" "github.com/plgd-dev/hub/v2/grpc-gateway/client" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" "github.com/plgd-dev/hub/v2/grpc-gateway/service" grpcgwService "github.com/plgd-dev/hub/v2/grpc-gateway/test" isEvents "github.com/plgd-dev/hub/v2/identity-store/events" - idService "github.com/plgd-dev/hub/v2/identity-store/test" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" @@ -656,20 +654,19 @@ func TestRequestHandlerSubscribeForPendingCommands(t *testing.T) { defer cancel() hubTestService.ClearDB(ctx, t) - oauthShutdown := oauthTest.SetUp(t) - authShutdown := idService.SetUp(t) - raShutdown := raTest.SetUp(t) - rdShutdown := rdTest.SetUp(t) - grpcShutdown := grpcgwService.SetUp(t) - caShutdown := caService.SetUp(t) - secureGWShutdown := coapgwTest.SetUp(t) - defer caShutdown() - defer grpcShutdown() - defer rdShutdown() - defer raShutdown() - defer authShutdown() - defer oauthShutdown() + const services = hubTestService.SetUpServicesMachine2MachineOAuth | hubTestService.SetUpServicesOAuth | hubTestService.SetUpServicesId | hubTestService.SetUpServicesResourceAggregate | + hubTestService.SetUpServicesResourceDirectory | hubTestService.SetUpServicesCertificateAuthority | hubTestService.SetUpServicesGrpcGateway + tearDown := hubTestService.SetUpServices(ctx, t, services) + defer tearDown() + + deferedSecureGWShutdown := true + secureGWShutdown := coapgwTest.SetUp(t) + defer func() { + if deferedSecureGWShutdown { + secureGWShutdown() + } + }() ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) @@ -688,6 +685,7 @@ func TestRequestHandlerSubscribeForPendingCommands(t *testing.T) { deviceID, shutdownDevSim := test.OnboardDevSim(ctx, t, c, deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, test.GetAllBackendResourceLinks()) defer shutdownDevSim() + deferedSecureGWShutdown = false secureGWShutdown() createFn := func(timeToLive time.Duration) { @@ -1003,7 +1001,8 @@ func TestCoAPGatewayServiceHeartbeat(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute*3) defer cancel() - tearDown := hubTestService.SetUpServices(ctx, t, hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesGrpcGateway|hubTestService.SetUpServicesId|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth) + tearDown := hubTestService.SetUpServices(ctx, t, hubTestService.SetUpServicesMachine2MachineOAuth|hubTestService.SetUpServicesCertificateAuthority|hubTestService.SetUpServicesGrpcGateway| + hubTestService.SetUpServicesId|hubTestService.SetUpServicesResourceDirectory|hubTestService.SetUpServicesOAuth) defer tearDown() racfg := raTest.MakeConfig(t) diff --git a/http-gateway/config.yaml b/http-gateway/config.yaml index b2bf6276f..fe109f644 100644 --- a/http-gateway/config.yaml +++ b/http-gateway/config.yaml @@ -35,6 +35,9 @@ apis: keyFile: "/secrets/private/cert.key" certFile: "/secrets/public/cert.crt" useSystemCAPool: false + tokenTrustVerification: + enabled: true + cacheExpiration: 30s clients: grpcGateway: grpc: @@ -77,36 +80,36 @@ ui: clientID: "" audience: "" scopes: [] - grantTypes: [ "authorization_code" ] + grantTypes: ["authorization_code"] deviceOAuthClient: authority: "" clientID: "" audience: "" scopes: [] providerName: "" - grantTypes: [ "authorization_code" ] + grantTypes: ["authorization_code"] m2mOAuthClient: authority: "" clientID: "" audience: "" scopes: [] providerName: "" - grantTypes: [ "client_credentials" ] + grantTypes: ["client_credentials"] useJWTPrivateKey: true visibility: mainSidebar: - certificates : true - chatRoom : true - configuration : true - deviceProvisioning : true - devices : true - docs : true - pendingCommands : true - remoteClients : true - dashboard : false - integrations : false - deviceFirmwareUpdate : false - deviceLogs : false - apiTokens : false - schemaHub : false + certificates: true + chatRoom: true + configuration: true + deviceProvisioning: true + devices: true + docs: true + pendingCommands: true + remoteClients: true + dashboard: false + integrations: false + deviceFirmwareUpdate: false + deviceLogs: false + apiTokens: false + schemaHub: false snippetService: true diff --git a/http-gateway/serverMux/router.go b/http-gateway/serverMux/router.go index 58651a566..4983120dc 100644 --- a/http-gateway/serverMux/router.go +++ b/http-gateway/serverMux/router.go @@ -6,18 +6,19 @@ import ( router "github.com/gorilla/mux" "github.com/plgd-dev/hub/v2/pkg/net/grpc" - pktHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpJwt "github.com/plgd-dev/hub/v2/pkg/net/http/jwt" "google.golang.org/grpc/codes" ) // NewRouter creates router with default middlewares -func NewRouter(queryCaseInsensitive map[string]string, authInterceptor pktHttp.Interceptor, opts ...pktHttp.LogOpt) *router.Router { +func NewRouter(queryCaseInsensitive map[string]string, authInterceptor pkgHttpJwt.Interceptor, opts ...pkgHttp.LogOpt) *router.Router { r := router.NewRouter() - r.Use(pktHttp.CreateLoggingMiddleware(opts...)) - r.Use(pktHttp.CreateAuthMiddleware(authInterceptor, func(_ context.Context, w http.ResponseWriter, r *http.Request, err error) { + r.Use(pkgHttp.CreateLoggingMiddleware(opts...)) + r.Use(pkgHttp.CreateAuthMiddleware(authInterceptor, func(_ context.Context, w http.ResponseWriter, r *http.Request, err error) { WriteError(w, grpc.ForwardErrorf(codes.Unauthenticated, "cannot access to %v: %w", r.RequestURI, err)) })) - r.Use(pktHttp.CreateMakeQueryCaseInsensitiveMiddleware(queryCaseInsensitive, opts...)) - r.Use(pktHttp.CreateTrailSlashSuffixMiddleware(opts...)) + r.Use(pkgHttp.CreateMakeQueryCaseInsensitiveMiddleware(queryCaseInsensitive, opts...)) + r.Use(pkgHttp.CreateTrailSlashSuffixMiddleware(opts...)) return r } diff --git a/http-gateway/service/cancelPendingCommands_test.go b/http-gateway/service/cancelPendingCommands_test.go index f6aa07fec..c7f850820 100644 --- a/http-gateway/service/cancelPendingCommands_test.go +++ b/http-gateway/service/cancelPendingCommands_test.go @@ -9,8 +9,8 @@ import ( httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" "github.com/plgd-dev/hub/v2/http-gateway/uri" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" "github.com/stretchr/testify/assert" @@ -23,7 +23,7 @@ func doPendingCommand(t *testing.T, request *http.Request) (*pb.CancelPendingCom _ = resp.Body.Close() }() var v pb.CancelPendingCommandsResponse - err := httpTest.Unmarshal(resp.StatusCode, resp.Body, &v) + err := pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &v) return &v, resp.StatusCode, err } diff --git a/http-gateway/service/createResource_test.go b/http-gateway/service/createResource_test.go index be2f188e2..637f15831 100644 --- a/http-gateway/service/createResource_test.go +++ b/http-gateway/service/createResource_test.go @@ -17,6 +17,7 @@ import ( "github.com/plgd-dev/hub/v2/http-gateway/uri" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" httpTest "github.com/plgd-dev/hub/v2/test/http" @@ -214,7 +215,7 @@ func TestRequestHandler_CreateResource(t *testing.T) { assert.Equal(t, tt.wantHTTPCode, resp.StatusCode) var got pb.CreateResourceResponse - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) if tt.wantErr { require.Error(t, err) assert.Equal(t, tt.wantErrCode.String(), status.Convert(err).Code().String()) diff --git a/http-gateway/service/deleteDevices_test.go b/http-gateway/service/deleteDevices_test.go index 0ac875b87..6dcdf9542 100644 --- a/http-gateway/service/deleteDevices_test.go +++ b/http-gateway/service/deleteDevices_test.go @@ -10,9 +10,9 @@ import ( httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" "github.com/plgd-dev/hub/v2/http-gateway/uri" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" "github.com/plgd-dev/hub/v2/test/service" "github.com/stretchr/testify/assert" @@ -92,7 +92,7 @@ func TestRequestHandlerDeleteDevices(t *testing.T) { assert.Equal(t, tt.wantHTTPCode, resp.StatusCode) var got pb.DeleteDevicesResponse - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) require.NoError(t, err) require.Equal(t, tt.want.GetDeviceIds(), got.GetDeviceIds()) }) diff --git a/http-gateway/service/deleteResource_test.go b/http-gateway/service/deleteResource_test.go index 5ca97d743..5a3bc73be 100644 --- a/http-gateway/service/deleteResource_test.go +++ b/http-gateway/service/deleteResource_test.go @@ -17,11 +17,11 @@ import ( "github.com/plgd-dev/hub/v2/http-gateway/uri" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" "github.com/plgd-dev/hub/v2/test/service" @@ -144,7 +144,7 @@ func TestRequestHandlerDeleteResource(t *testing.T) { assert.Equal(t, tt.wantHTTPCode, resp.StatusCode) var got pb.DeleteResourceResponse - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) if tt.wantErr { require.Error(t, err) assert.Equal(t, tt.wantErrCode.String(), exCodes.Code(status.Convert(err).Code()).String()) @@ -254,7 +254,7 @@ func TestRequestHandlerBatchDeleteResource(t *testing.T) { assert.Equal(t, tt.wantHTTPCode, resp.StatusCode) var got pb.DeleteResourceResponse - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) if tt.wantErr { require.Error(t, err) assert.Equal(t, tt.wantErrCode.String(), exCodes.Code(status.Convert(err).Code()).String()) diff --git a/http-gateway/service/getDevicePendingCommands_test.go b/http-gateway/service/getDevicePendingCommands_test.go index 28c906605..40715d1f8 100644 --- a/http-gateway/service/getDevicePendingCommands_test.go +++ b/http-gateway/service/getDevicePendingCommands_test.go @@ -12,22 +12,18 @@ import ( "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/schema/platform" "github.com/plgd-dev/go-coap/v3/message" - caService "github.com/plgd-dev/hub/v2/certificate-authority/test" coapgwTest "github.com/plgd-dev/hub/v2/coap-gateway/test" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" - grpcgwService "github.com/plgd-dev/hub/v2/grpc-gateway/test" httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" "github.com/plgd-dev/hub/v2/http-gateway/uri" - idService "github.com/plgd-dev/hub/v2/identity-store/test" + "github.com/plgd-dev/hub/v2/pkg/fn" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" - raService "github.com/plgd-dev/hub/v2/resource-aggregate/test" - rdService "github.com/plgd-dev/hub/v2/resource-directory/test" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" "github.com/plgd-dev/hub/v2/test/oauth-server/service" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" @@ -217,20 +213,20 @@ func TestRequestHandlerGetDevicePendingCommands(t *testing.T) { defer cancel() testService.ClearDB(ctx, t) - oauthShutdown := oauthTest.SetUp(t) - idShutdown := idService.SetUp(t) - raShutdown := raService.SetUp(t) - rdShutdown := rdService.SetUp(t) - grpcShutdown := grpcgwService.SetUp(t) - caShutdown := caService.SetUp(t) - secureGWShutdown := coapgwTest.SetUp(t) - defer caShutdown() - defer grpcShutdown() - defer rdShutdown() - defer raShutdown() - defer idShutdown() - defer oauthShutdown() + var closeFunc fn.FuncList + defer closeFunc.Execute() + tearDown := testService.SetUpServices(ctx, t, testService.SetUpServicesOAuth|testService.SetUpServicesMachine2MachineOAuth|testService.SetUpServicesId|testService.SetUpServicesResourceAggregate| + testService.SetUpServicesResourceDirectory|testService.SetUpServicesCertificateAuthority|testService.SetUpServicesGrpcGateway) + closeFunc.AddFunc(tearDown) + + deferedSecureGWShutdown := true + secureGWShutdown := coapgwTest.SetUp(t) + defer func() { + if deferedSecureGWShutdown { + secureGWShutdown() + } + }() shutdownHttp := httpgwTest.SetUp(t) defer shutdownHttp() @@ -250,6 +246,7 @@ func TestRequestHandlerGetDevicePendingCommands(t *testing.T) { deviceID, shutdownDevSim := test.OnboardDevSim(ctx, t, c, deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, test.GetAllBackendResourceLinks()) defer shutdownDevSim() + deferedSecureGWShutdown = false secureGWShutdown() createFn := func() { @@ -320,7 +317,7 @@ func TestRequestHandlerGetDevicePendingCommands(t *testing.T) { var values []*pb.PendingCommand for { var v pb.PendingCommand - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &v) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &v) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getDeviceResourceLinks_test.go b/http-gateway/service/getDeviceResourceLinks_test.go index 13a8a650c..474b9cd4e 100644 --- a/http-gateway/service/getDeviceResourceLinks_test.go +++ b/http-gateway/service/getDeviceResourceLinks_test.go @@ -17,11 +17,11 @@ import ( httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" "github.com/plgd-dev/hub/v2/http-gateway/uri" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" test "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" @@ -120,7 +120,7 @@ func TestRequestHandlerGetDeviceResourceLinks(t *testing.T) { var links []*events.ResourceLinksPublished for { var v events.ResourceLinksPublished - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &v) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &v) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getDeviceResources_test.go b/http-gateway/service/getDeviceResources_test.go index 9156ee151..35b2c95f9 100644 --- a/http-gateway/service/getDeviceResources_test.go +++ b/http-gateway/service/getDeviceResources_test.go @@ -19,11 +19,11 @@ import ( "github.com/plgd-dev/hub/v2/http-gateway/uri" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" "github.com/plgd-dev/hub/v2/test/service" @@ -182,7 +182,7 @@ func TestRequestHandlerGetDeviceResources(t *testing.T) { values := make([]*pb.Resource, 0, 1) for { var value pb.Resource - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &value) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &value) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getDevice_test.go b/http-gateway/service/getDevice_test.go index 3584b6062..a118f9df5 100644 --- a/http-gateway/service/getDevice_test.go +++ b/http-gateway/service/getDevice_test.go @@ -13,9 +13,9 @@ import ( httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" "github.com/plgd-dev/hub/v2/http-gateway/uri" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" "github.com/plgd-dev/hub/v2/test/service" @@ -93,7 +93,7 @@ func TestRequestHandlerGetDevice(t *testing.T) { devices := make([]*pb.Device, 0, 1) for { var dev pb.Device - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &dev) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &dev) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getDevicesMetadata_test.go b/http-gateway/service/getDevicesMetadata_test.go index d6dde4980..28d319add 100644 --- a/http-gateway/service/getDevicesMetadata_test.go +++ b/http-gateway/service/getDevicesMetadata_test.go @@ -13,11 +13,11 @@ import ( httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" "github.com/plgd-dev/hub/v2/http-gateway/uri" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" @@ -146,7 +146,7 @@ func TestRequestHandlerGetDevicesMetadata(t *testing.T) { var values []*events.DeviceMetadataUpdated for { var value events.DeviceMetadataUpdated - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &value) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &value) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getDevices_test.go b/http-gateway/service/getDevices_test.go index f2c3fd248..67cd41d16 100644 --- a/http-gateway/service/getDevices_test.go +++ b/http-gateway/service/getDevices_test.go @@ -18,10 +18,10 @@ import ( "github.com/plgd-dev/hub/v2/http-gateway/uri" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" "github.com/plgd-dev/hub/v2/test/service" @@ -155,7 +155,7 @@ func TestRequestHandlerGetDevices(t *testing.T) { var devices []*pb.Device for { var dev pb.Device - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &dev) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &dev) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getEvents_test.go b/http-gateway/service/getEvents_test.go index e2497c9e6..e29db1129 100644 --- a/http-gateway/service/getEvents_test.go +++ b/http-gateway/service/getEvents_test.go @@ -14,9 +14,9 @@ import ( "github.com/plgd-dev/hub/v2/http-gateway/uri" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" "github.com/plgd-dev/hub/v2/test/service" @@ -174,7 +174,7 @@ func TestRequestHandlerGetEvents(t *testing.T) { values := make([]*pb.GetEventsResponse, 0, 1) for { var value pb.GetEventsResponse - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &value) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &value) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getHubConfiguration_test.go b/http-gateway/service/getHubConfiguration_test.go index fba747935..41b9d40ed 100644 --- a/http-gateway/service/getHubConfiguration_test.go +++ b/http-gateway/service/getHubConfiguration_test.go @@ -9,9 +9,9 @@ import ( httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" "github.com/plgd-dev/hub/v2/http-gateway/uri" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" rdTest "github.com/plgd-dev/hub/v2/resource-directory/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" pbTest "github.com/plgd-dev/hub/v2/test/pb" "github.com/plgd-dev/hub/v2/test/service" "github.com/stretchr/testify/require" @@ -65,7 +65,7 @@ func TestRequestHandlerGetHubConfiguration(t *testing.T) { _ = resp.Body.Close() }() var got pb.HubConfigurationResponse - err := httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err := pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) require.NoError(t, err) pbTest.CmpHubConfigurationResponse(t, tt.want, &got) }) @@ -121,7 +121,7 @@ func TestRequestHandlerGetHubConfigurationWithoutM2MOAuthClient(t *testing.T) { _ = resp.Body.Close() }() var got pb.HubConfigurationResponse - err := httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err := pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) require.NoError(t, err) pbTest.CmpHubConfigurationResponse(t, tt.want, &got) }) diff --git a/http-gateway/service/getPendingCommands_test.go b/http-gateway/service/getPendingCommands_test.go index a39ca1c2f..f1ff93b8f 100644 --- a/http-gateway/service/getPendingCommands_test.go +++ b/http-gateway/service/getPendingCommands_test.go @@ -12,22 +12,18 @@ import ( "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/schema/platform" "github.com/plgd-dev/go-coap/v3/message" - caService "github.com/plgd-dev/hub/v2/certificate-authority/test" coapgwTest "github.com/plgd-dev/hub/v2/coap-gateway/test" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" - grpcgwService "github.com/plgd-dev/hub/v2/grpc-gateway/test" httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" "github.com/plgd-dev/hub/v2/http-gateway/uri" - idService "github.com/plgd-dev/hub/v2/identity-store/test" + "github.com/plgd-dev/hub/v2/pkg/fn" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" - raService "github.com/plgd-dev/hub/v2/resource-aggregate/test" - rdService "github.com/plgd-dev/hub/v2/resource-directory/test" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" "github.com/plgd-dev/hub/v2/test/oauth-server/service" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" @@ -264,20 +260,20 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { defer cancel() testService.ClearDB(ctx, t) - oauthShutdown := oauthTest.SetUp(t) - idShutdown := idService.SetUp(t) - raShutdown := raService.SetUp(t) - rdShutdown := rdService.SetUp(t) - grpcShutdown := grpcgwService.SetUp(t) - caShutdown := caService.SetUp(t) - secureGWShutdown := coapgwTest.SetUp(t) - defer caShutdown() - defer grpcShutdown() - defer rdShutdown() - defer raShutdown() - defer idShutdown() - defer oauthShutdown() + var closeFunc fn.FuncList + defer closeFunc.Execute() + tearDown := testService.SetUpServices(ctx, t, testService.SetUpServicesOAuth|testService.SetUpServicesMachine2MachineOAuth|testService.SetUpServicesId|testService.SetUpServicesResourceAggregate| + testService.SetUpServicesResourceDirectory|testService.SetUpServicesCertificateAuthority|testService.SetUpServicesGrpcGateway) + closeFunc.AddFunc(tearDown) + + deferedSecureGWShutdown := true + secureGWShutdown := coapgwTest.SetUp(t) + defer func() { + if deferedSecureGWShutdown { + secureGWShutdown() + } + }() shutdownHttp := httpgwTest.SetUp(t) defer shutdownHttp() @@ -297,6 +293,7 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { deviceID, shutdownDevSim := test.OnboardDevSim(ctx, t, c, deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, test.GetAllBackendResourceLinks()) defer shutdownDevSim() + deferedSecureGWShutdown = false secureGWShutdown() createFn := func() { @@ -368,7 +365,7 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { var values []*pb.PendingCommand for { var v pb.PendingCommand - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &v) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &v) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getPendingMetadataUpdates_test.go b/http-gateway/service/getPendingMetadataUpdates_test.go index a53297447..ab1eb4ead 100644 --- a/http-gateway/service/getPendingMetadataUpdates_test.go +++ b/http-gateway/service/getPendingMetadataUpdates_test.go @@ -12,22 +12,18 @@ import ( "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/schema/platform" "github.com/plgd-dev/go-coap/v3/message" - caService "github.com/plgd-dev/hub/v2/certificate-authority/test" coapgwTest "github.com/plgd-dev/hub/v2/coap-gateway/test" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" - grpcgwService "github.com/plgd-dev/hub/v2/grpc-gateway/test" httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" "github.com/plgd-dev/hub/v2/http-gateway/uri" - idService "github.com/plgd-dev/hub/v2/identity-store/test" + "github.com/plgd-dev/hub/v2/pkg/fn" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" - raService "github.com/plgd-dev/hub/v2/resource-aggregate/test" - rdService "github.com/plgd-dev/hub/v2/resource-directory/test" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" "github.com/plgd-dev/hub/v2/test/oauth-server/service" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" @@ -74,20 +70,19 @@ func TestRequestHandlerGetPendingMetadataUpdates(t *testing.T) { defer cancel() testService.ClearDB(ctx, t) - oauthShutdown := oauthTest.SetUp(t) - idShutdown := idService.SetUp(t) - raShutdown := raService.SetUp(t) - rdShutdown := rdService.SetUp(t) - grpcShutdown := grpcgwService.SetUp(t) - caShutdown := caService.SetUp(t) - secureGWShutdown := coapgwTest.SetUp(t) + var closeFunc fn.FuncList + defer closeFunc.Execute() + tearDown := testService.SetUpServices(ctx, t, testService.SetUpServicesOAuth|testService.SetUpServicesMachine2MachineOAuth|testService.SetUpServicesId|testService.SetUpServicesResourceAggregate| + testService.SetUpServicesResourceDirectory|testService.SetUpServicesCertificateAuthority|testService.SetUpServicesGrpcGateway) + closeFunc.AddFunc(tearDown) - defer caShutdown() - defer grpcShutdown() - defer rdShutdown() - defer raShutdown() - defer idShutdown() - defer oauthShutdown() + deferedSecureGWShutdown := true + secureGWShutdown := coapgwTest.SetUp(t) + defer func() { + if deferedSecureGWShutdown { + secureGWShutdown() + } + }() shutdownHttp := httpgwTest.SetUp(t) defer shutdownHttp() @@ -107,6 +102,7 @@ func TestRequestHandlerGetPendingMetadataUpdates(t *testing.T) { deviceID, shutdownDevSim := test.OnboardDevSim(ctx, t, c, deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, test.GetAllBackendResourceLinks()) defer shutdownDevSim() + deferedSecureGWShutdown = false secureGWShutdown() createFn := func() { @@ -181,7 +177,7 @@ func TestRequestHandlerGetPendingMetadataUpdates(t *testing.T) { for { var v pb.PendingCommand - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &v) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &v) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getResourceLinks_test.go b/http-gateway/service/getResourceLinks_test.go index 0d62c3734..afecab1cc 100644 --- a/http-gateway/service/getResourceLinks_test.go +++ b/http-gateway/service/getResourceLinks_test.go @@ -16,11 +16,11 @@ import ( httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" "github.com/plgd-dev/hub/v2/http-gateway/uri" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" test "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" @@ -112,7 +112,7 @@ func TestRequestHandlerGetResourceLinks(t *testing.T) { var links []*events.ResourceLinksPublished for { var v events.ResourceLinksPublished - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &v) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &v) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getResourcePendingCommands_test.go b/http-gateway/service/getResourcePendingCommands_test.go index d75438626..e02772e3e 100644 --- a/http-gateway/service/getResourcePendingCommands_test.go +++ b/http-gateway/service/getResourcePendingCommands_test.go @@ -12,22 +12,18 @@ import ( "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/schema/platform" "github.com/plgd-dev/go-coap/v3/message" - caService "github.com/plgd-dev/hub/v2/certificate-authority/test" coapgwTest "github.com/plgd-dev/hub/v2/coap-gateway/test" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" - grpcgwService "github.com/plgd-dev/hub/v2/grpc-gateway/test" httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" "github.com/plgd-dev/hub/v2/http-gateway/uri" - idService "github.com/plgd-dev/hub/v2/identity-store/test" + "github.com/plgd-dev/hub/v2/pkg/fn" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" - raService "github.com/plgd-dev/hub/v2/resource-aggregate/test" - rdService "github.com/plgd-dev/hub/v2/resource-directory/test" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" "github.com/plgd-dev/hub/v2/test/oauth-server/service" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" @@ -129,20 +125,19 @@ func TestRequestHandlerGetResourcePendingCommands(t *testing.T) { defer cancel() testService.ClearDB(ctx, t) - oauthShutdown := oauthTest.SetUp(t) - idShutdown := idService.SetUp(t) - raShutdown := raService.SetUp(t) - rdShutdown := rdService.SetUp(t) - grpcShutdown := grpcgwService.SetUp(t) - caShutdown := caService.SetUp(t) - secureGWShutdown := coapgwTest.SetUp(t) + var closeFunc fn.FuncList + defer closeFunc.Execute() + tearDown := testService.SetUpServices(ctx, t, testService.SetUpServicesOAuth|testService.SetUpServicesMachine2MachineOAuth|testService.SetUpServicesId|testService.SetUpServicesResourceAggregate| + testService.SetUpServicesResourceDirectory|testService.SetUpServicesCertificateAuthority|testService.SetUpServicesGrpcGateway) + closeFunc.AddFunc(tearDown) - defer caShutdown() - defer grpcShutdown() - defer rdShutdown() - defer raShutdown() - defer idShutdown() - defer oauthShutdown() + deferedSecureGWShutdown := true + secureGWShutdown := coapgwTest.SetUp(t) + defer func() { + if deferedSecureGWShutdown { + secureGWShutdown() + } + }() shutdownHttp := httpgwTest.SetUp(t) defer shutdownHttp() @@ -162,6 +157,7 @@ func TestRequestHandlerGetResourcePendingCommands(t *testing.T) { deviceID, shutdownDevSim := test.OnboardDevSim(ctx, t, c, deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, test.GetAllBackendResourceLinks()) defer shutdownDevSim() + deferedSecureGWShutdown = false secureGWShutdown() createFn := func() { @@ -232,7 +228,7 @@ func TestRequestHandlerGetResourcePendingCommands(t *testing.T) { var values []*pb.PendingCommand for { var v pb.PendingCommand - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &v) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &v) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getResource_test.go b/http-gateway/service/getResource_test.go index 55343c283..e50c4fd2d 100644 --- a/http-gateway/service/getResource_test.go +++ b/http-gateway/service/getResource_test.go @@ -17,11 +17,11 @@ import ( "github.com/plgd-dev/hub/v2/http-gateway/uri" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" @@ -201,7 +201,7 @@ func TestRequestHandlerGetResource(t *testing.T) { values := make([]*events.ResourceRetrieved, 0, 1) for { var value pb.GetResourceFromDeviceResponse - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &value) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &value) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getResources_test.go b/http-gateway/service/getResources_test.go index e531ec823..6d576500c 100644 --- a/http-gateway/service/getResources_test.go +++ b/http-gateway/service/getResources_test.go @@ -17,11 +17,11 @@ import ( "github.com/plgd-dev/hub/v2/http-gateway/uri" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" @@ -229,7 +229,7 @@ func TestRequestHandlerGetResources(t *testing.T) { values := make([]*pb.Resource, 0, 1) for { var value pb.Resource - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &value) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &value) if errors.Is(err, io.EOF) { break } diff --git a/http-gateway/service/getThings.go b/http-gateway/service/getThings.go index 59012a046..311417e4d 100644 --- a/http-gateway/service/getThings.go +++ b/http-gateway/service/getThings.go @@ -329,12 +329,15 @@ func ThingSetSecurity(td *wotTD.ThingDescription, openIDConfigs []openid.Config) td.Security = &wotTD.TypeDeclaration{} td.SecurityDefinitions = make(map[string]wotTD.SecurityScheme) for idx := range openIDConfigs { - td.SecurityDefinitions[toSecurityName(idx)] = wotTD.SecurityScheme{ - Scheme: "oauth2", - Flow: bridgeDeviceTD.StringToPtr("code"), - Authorization: &(openIDConfigs[idx].AuthURL), - Token: &(openIDConfigs[idx].TokenURL), + ss := wotTD.SecurityScheme{ + Scheme: "oauth2", + Flow: bridgeDeviceTD.StringToPtr("code"), + Token: &(openIDConfigs[idx].TokenURL), } + if openIDConfigs[idx].AuthURL != "" { + ss.Authorization = &(openIDConfigs[idx].AuthURL) + } + td.SecurityDefinitions[toSecurityName(idx)] = ss td.Security.StringArray = append(td.Security.StringArray, toSecurityName(idx)) } } diff --git a/http-gateway/service/getThings_test.go b/http-gateway/service/getThings_test.go index 333a05f82..e829e538a 100644 --- a/http-gateway/service/getThings_test.go +++ b/http-gateway/service/getThings_test.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "testing" "github.com/google/uuid" @@ -37,6 +38,7 @@ import ( "github.com/plgd-dev/hub/v2/test/device/bridge" httpTest "github.com/plgd-dev/hub/v2/test/http" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + oauthUri "github.com/plgd-dev/hub/v2/test/oauth-server/uri" "github.com/plgd-dev/hub/v2/test/service" vd "github.com/plgd-dev/hub/v2/test/virtual-device" "github.com/stretchr/testify/require" @@ -102,7 +104,7 @@ func TestRequestHandlerGetThings(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - const services = service.SetUpServicesOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | + const services = service.SetUpServicesOAuth | service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesId | service.SetUpServicesResourceDirectory | service.SetUpServicesGrpcGateway | service.SetUpServicesResourceAggregate isConfig := isTest.MakeConfig(t) isConfig.APIs.GRPC.TLS.ClientCertificateRequired = false @@ -200,6 +202,28 @@ func TestBridgeDeviceGetThings(t *testing.T) { } } +func patchSecurity(td *wotTD.ThingDescription) { + vCfg := config.MakeValidatorConfig() + openCfgs := make([]openid.Config, 0, len(vCfg.Endpoints)) + for _, ep := range vCfg.Endpoints { + host := ep.Authority + if strings.Contains(host, config.OAUTH_SERVER_HOST) { + openCfgs = append(openCfgs, openid.Config{ + TokenURL: ep.Authority + oauthUri.Token, + AuthURL: ep.Authority + oauthUri.Authorize, + }) + continue + } + if strings.Contains(host, config.M2M_OAUTH_SERVER_HTTP_HOST) { + openCfgs = append(openCfgs, openid.Config{ + TokenURL: ep.Authority + oauthUri.Token, + }) + continue + } + } + httpgwService.ThingSetSecurity(td, openCfgs) +} + func getPatchedTD(t *testing.T, deviceCfg bridgeDevice.Config, deviceID string, links []wotTD.IconLinkElement, validateDevices map[string]struct{}, title, host string) *wotTD.ThingDescription { td, err := bridgeDevice.GetThingDescription(deviceCfg.ThingDescription.File, deviceCfg.NumResourcesPerDevice) require.NoError(t, err) @@ -226,10 +250,7 @@ func getPatchedTD(t *testing.T, deviceCfg bridgeDevice.Config, deviceID string, } td.Properties[schemaDevice.ResourceURI] = dev - httpgwService.ThingSetSecurity(&td, []openid.Config{{ - TokenURL: "https://localhost:20009/oauth/token", - AuthURL: "https://localhost:20009/authorize", - }}) + patchSecurity(&td) mnt, ok := bridgeResourcesTD.GetOCFResourcePropertyElement(schemaMaintenance.ResourceURI) require.True(t, ok) diff --git a/http-gateway/service/requestHandler.go b/http-gateway/service/requestHandler.go index dd53adccb..affab337a 100644 --- a/http-gateway/service/requestHandler.go +++ b/http-gateway/service/requestHandler.go @@ -16,6 +16,7 @@ import ( "github.com/plgd-dev/hub/v2/http-gateway/uri" "github.com/plgd-dev/hub/v2/pkg/log" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpUri "github.com/plgd-dev/hub/v2/pkg/net/http/uri" "github.com/plgd-dev/hub/v2/pkg/security/openid" pkgStrings "github.com/plgd-dev/hub/v2/pkg/strings" ) @@ -33,7 +34,7 @@ func matchPrefixAndSplitURIPath(requestURI, prefix string) []string { if len(requestURI) == 0 { return nil } - v := pkgHttp.CanonicalHref(requestURI) + v := pkgHttpUri.CanonicalHref(requestURI) p := strings.TrimPrefix(v, prefix) // remove core prefix if p == v { return nil diff --git a/http-gateway/service/service.go b/http-gateway/service/service.go index bc96556e7..7959e14f6 100644 --- a/http-gateway/service/service.go +++ b/http-gateway/service/service.go @@ -13,7 +13,8 @@ import ( "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" grpcClient "github.com/plgd-dev/hub/v2/pkg/net/grpc/client" - kitNetHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpJwt "github.com/plgd-dev/hub/v2/pkg/net/http/jwt" httpService "github.com/plgd-dev/hub/v2/pkg/net/http/service" otelClient "github.com/plgd-dev/hub/v2/pkg/opentelemetry/collector/client" "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" @@ -38,7 +39,7 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg return nil, fmt.Errorf("cannot create validator: %w", err) } - whiteList := []kitNetHttp.RequestMatcher{ + whiteList := []pkgHttpJwt.RequestMatcher{ { Method: http.MethodGet, URI: regexp.MustCompile(regexp.QuoteMeta(uri.APIWS) + `.*`), @@ -49,7 +50,7 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg }, } if config.UI.Enabled { - whiteList = append(whiteList, kitNetHttp.RequestMatcher{ + whiteList = append(whiteList, pkgHttpJwt.RequestMatcher{ Method: http.MethodGet, URI: regexp.MustCompile(AuthorizationWhiteListedEndpointsRegexp), }) @@ -58,7 +59,7 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg HTTPConnection: config.APIs.HTTP.Connection, HTTPServer: config.APIs.HTTP.Server, ServiceName: serviceName, - AuthRules: kitNetHttp.NewDefaultAuthorizationRules(uri.API), + AuthRules: pkgHttp.NewDefaultAuthorizationRules(uri.API), WhiteEndpointList: whiteList, FileWatcher: fileWatcher, Logger: logger, diff --git a/http-gateway/service/subscribeToEvents_test.go b/http-gateway/service/subscribeToEvents_test.go index 24b5d2a2d..235d5a626 100644 --- a/http-gateway/service/subscribeToEvents_test.go +++ b/http-gateway/service/subscribeToEvents_test.go @@ -19,6 +19,7 @@ import ( isEvents "github.com/plgd-dev/hub/v2/identity-store/events" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" natsClient "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventbus/nats/client" "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventbus/nats/publisher" @@ -27,7 +28,6 @@ import ( rdTest "github.com/plgd-dev/hub/v2/resource-directory/test" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" - httpTest "github.com/plgd-dev/hub/v2/test/http" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" pbTest "github.com/plgd-dev/hub/v2/test/pb" "github.com/plgd-dev/hub/v2/test/service" @@ -212,7 +212,7 @@ func testRequestHandlerSubscribeToEvents(t *testing.T, deviceID string, resource return nil, errM } var event pb.Event - errM = httpTest.Unmarshal(http.StatusOK, reader, &event) + errM = pkgHttpPb.Unmarshal(http.StatusOK, reader, &event) return &event, errM } diff --git a/http-gateway/service/updateDeviceMetadata_test.go b/http-gateway/service/updateDeviceMetadata_test.go index 1e93fd028..473cc1636 100644 --- a/http-gateway/service/updateDeviceMetadata_test.go +++ b/http-gateway/service/updateDeviceMetadata_test.go @@ -18,6 +18,7 @@ import ( "github.com/plgd-dev/hub/v2/pkg/log" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventbus" "github.com/plgd-dev/hub/v2/resource-aggregate/cqrs/eventbus/nats/subscriber" @@ -107,7 +108,7 @@ func updateResource(t *testing.T, req *pb.UpdateResourceRequest, token string) e }() var got pb.UpdateResourceResponse - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) if err != nil { return err } @@ -175,7 +176,7 @@ func TestRequestHandlerUpdateDeviceMetadata(t *testing.T) { }(resp) var got pb.UpdateDeviceMetadataResponse - errM = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + errM = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) return errM } diff --git a/http-gateway/service/updateResource_test.go b/http-gateway/service/updateResource_test.go index f39a9f6ce..fb8e080bc 100644 --- a/http-gateway/service/updateResource_test.go +++ b/http-gateway/service/updateResource_test.go @@ -17,6 +17,7 @@ import ( "github.com/plgd-dev/hub/v2/http-gateway/uri" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" "github.com/plgd-dev/hub/v2/test" @@ -231,7 +232,7 @@ func TestRequestHandlerUpdateResourcesValues(t *testing.T) { assert.Equal(t, tt.wantHTTPCode, resp.StatusCode) var got pb.UpdateResourceResponse - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) if tt.wantErr { require.Error(t, err) return diff --git a/identity-store/client/ownerCache_test.go b/identity-store/client/ownerCache_test.go index 6ab95b03b..77f8313a1 100644 --- a/identity-store/client/ownerCache_test.go +++ b/identity-store/client/ownerCache_test.go @@ -36,12 +36,8 @@ func TestOwnerCacheSubscribe(t *testing.T) { devices := []string{test.GenerateDeviceIDbyIdx(1), test.GenerateDeviceIDbyIdx(2), test.GenerateDeviceIDbyIdx(3)} cfg := idService.MakeConfig(t) cfg.APIs.GRPC.Addr = "localhost:1234" - - oauthShutdown := oauthService.SetUp(t) - defer oauthShutdown() - - shutdown := idService.New(t, cfg) - defer shutdown() + tearDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth|service.SetUpServicesId, service.WithISConfig(cfg)) + defer tearDown() token := oauthService.GetDefaultAccessToken(t) diff --git a/identity-store/config.yaml b/identity-store/config.yaml index 1c1baa99d..528d54cb9 100644 --- a/identity-store/config.yaml +++ b/identity-store/config.yaml @@ -44,6 +44,9 @@ apis: keyFile: "/secrets/private/cert.key" certFile: "/secrets/public/cert.crt" useSystemCAPool: false + tokenTrustVerification: + enabled: true + cacheExpiration: 30s clients: eventBus: nats: diff --git a/m2m-oauth-server/Makefile b/m2m-oauth-server/Makefile index a0ae54877..04a971d21 100644 --- a/m2m-oauth-server/Makefile +++ b/m2m-oauth-server/Makefile @@ -6,6 +6,9 @@ ifneq ($(BRANCH_TAG),main) LATEST_TAG = $(BRANCH_TAG) endif VERSION_TAG ?= $(LATEST_TAG)-$(shell git rev-parse --short=7 --verify HEAD) +GOPATH ?= $(shell go env GOPATH) +WORKING_DIRECTORY := $(shell pwd) +REPOSITORY_DIRECTORY := $(shell cd .. && pwd) BUILD_COMMIT_DATE ?= $(shell date -u +%FT%TZ --date=@`git show --format='%ct' HEAD --quiet`) BUILD_SHORT_COMMIT ?= $(shell git show --format=%h HEAD --quiet) BUILD_DATE ?= $(shell date -u +%FT%TZ) @@ -14,7 +17,7 @@ BUILD_VERSION ?= $(shell git tag --sort version:refname | tail -1 | sed -e "s/^v default: build define build-docker-image - cd ../.. && \ + cd .. && \ mkdir -p .tmp/docker/$(SERVICE_NAME) && \ awk '{gsub("@NAME@","$(SERVICE_NAME)")} {gsub("@DIRECTORY@","m2m-oauth-server")} {print}' tools/docker/Dockerfile.in > .tmp/docker/$(SERVICE_NAME)/Dockerfile && \ docker build \ @@ -41,6 +44,22 @@ push: build-servicecontainer docker push plgd/$(SERVICE_NAME):$(VERSION_TAG) docker push plgd/$(SERVICE_NAME):$(LATEST_TAG) +GOOGLEAPIS_PATH := $(REPOSITORY_DIRECTORY)/dependency/googleapis +GRPCGATEWAY_MODULE_PATH := $(shell go list -m -f '{{.Dir}}' github.com/grpc-ecosystem/grpc-gateway/v2 | head -1) + proto/generate: + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --go_out=$(GOPATH)/src $(WORKING_DIRECTORY)/pb/service.proto + protoc-go-inject-tag -remove_tag_comment -input=$(WORKING_DIRECTORY)/pb/service.pb.go + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --openapiv2_out=$(REPOSITORY_DIRECTORY) \ + --openapiv2_opt logtostderr=true \ + $(WORKING_DIRECTORY)/pb/service.proto + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --grpc-gateway_out=$(REPOSITORY_DIRECTORY) \ + --grpc-gateway_opt logtostderr=true \ + --grpc-gateway_opt paths=source_relative \ + $(WORKING_DIRECTORY)/pb/service.proto + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --go-grpc_out=$(GOPATH)/src \ + $(WORKING_DIRECTORY)/pb/service.proto + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --doc_out=$(WORKING_DIRECTORY)/pb --doc_opt=markdown,README.md $(WORKING_DIRECTORY)/pb/*.proto + protoc -I=. -I=$(REPOSITORY_DIRECTORY) -I=$(GOPATH)/src -I=$(GOOGLEAPIS_PATH) -I=$(GRPCGATEWAY_MODULE_PATH) --doc_out=$(WORKING_DIRECTORY)/pb --doc_opt=html,doc.html $(WORKING_DIRECTORY)/pb/*.proto .PHONY: build-servicecontainer build push proto/generate diff --git a/m2m-oauth-server/config.yaml b/m2m-oauth-server/config.yaml index fec4b0d13..f05c7b7bf 100644 --- a/m2m-oauth-server/config.yaml +++ b/m2m-oauth-server/config.yaml @@ -1,25 +1,64 @@ log: - level: info - encoding: json - stacktrace: - enabled: false - level: warn - encoderConfig: - timeEncoder: rfc3339nano - dumpBody: false + level: info + encoding: json + stacktrace: + enabled: false + level: warn + encoderConfig: + timeEncoder: rfc3339nano apis: - http: + grpc: address: "0.0.0.0:9100" - readTimeout: 8s - readHeaderTimeout: 4s - writeTimeout: 16s - idleTimeout: 30s + sendMsgSize: 4194304 + recvMsgSize: 4194304 + enforcementPolicy: + minTime: 5s + permitWithoutStream: true + keepAlive: + # 0s - means infinity + maxConnectionIdle: 0s + # 0s - means infinity + maxConnectionAge: 0s + # 0s - means infinity + maxConnectionAgeGrace: 0s + time: 2h + timeout: 20s tls: caPool: "/secrets/public/rootca.crt" keyFile: "/secrets/private/cert.key" - certFile: "/secrets/public/cert.crt" + certFile: "/secrets/private/cert.crt" clientCertificateRequired: true + authorization: + ownerClaim: "sub" + authority: "" + audience: "" + http: + maxIdleConns: 16 + maxConnsPerHost: 32 + maxIdleConnsPerHost: 16 + idleConnTimeout: "30s" + timeout: "10s" + tls: + caPool: "/secrets/public/rootca.crt" + keyFile: "/secrets/private/cert.key" + certFile: "/secrets/public/cert.crt" + useSystemCAPool: false + tokenTrustVerification: + enabled: false clients: + storage: + cleanUpDeletedTokens: 0 * * * * + use: mongoDB + mongoDB: + uri: + database: m2mOAuthServer + maxPoolSize: 16 + maxConnIdleTime: 4m0s + tls: + caPool: "/secrets/public/rootca.crt" + keyFile: "/secrets/private/cert.key" + certFile: "/secrets/public/cert.crt" + useSystemCAPool: false openTelemetryCollector: grpc: enabled: false @@ -36,8 +75,8 @@ clients: certFile: "/secrets/public/cert.crt" useSystemCAPool: false oauthSigner: - privateKeyFile: "/secrets/private/private.key" - domain: - ownerClaim: sub - deviceIDClaim: - clients: \ No newline at end of file + privateKeyFile: "/secrets/private/private.key" + domain: + ownerClaim: sub + deviceIDClaim: + clients: diff --git a/m2m-oauth-server/oauthSigner/config.go b/m2m-oauth-server/oauthSigner/config.go new file mode 100644 index 000000000..000234bb9 --- /dev/null +++ b/m2m-oauth-server/oauthSigner/config.go @@ -0,0 +1,128 @@ +package oauthsigner + +import ( + "fmt" + "time" + + "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" + "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" +) + +type AccessTokenType string + +const AccessTokenType_JWT AccessTokenType = "jwt" + +type GrantType string + +const ( + GrantTypeClientCredentials GrantType = "client_credentials" +) + +type PrivateKeyJWTConfig struct { + Enabled bool `yaml:"enabled"` + Authorization validator.Config `yaml:"authorization,omitempty"` +} + +func (c *PrivateKeyJWTConfig) Validate() error { + if !c.Enabled { + return nil + } + if err := c.Authorization.Validate(); err != nil { + return fmt.Errorf("authorization.%w", err) + } + return nil +} + +type Client struct { + ID string `yaml:"id"` + SecretFile urischeme.URIScheme `yaml:"secretFile"` + Owner string `yaml:"owner"` + AccessTokenLifetime time.Duration `yaml:"accessTokenLifetime"` + AllowedGrantTypes []GrantType `yaml:"allowedGrantTypes"` + AllowedAudiences []string `yaml:"allowedAudiences"` + AllowedScopes []string `yaml:"allowedScopes"` + JWTPrivateKey PrivateKeyJWTConfig `yaml:"jwtPrivateKey"` + InsertTokenClaims map[string]interface{} `yaml:"insertTokenClaims"` + + // runtime + Secret string `yaml:"-"` +} + +func (c *Client) Validate() error { + if c.ID == "" { + return fmt.Errorf("id('%v')", c.ID) + } + if !c.JWTPrivateKey.Enabled { + if c.SecretFile == "" { + return fmt.Errorf("secretFile('%v') - one of [secretFile, privateKeyJWT] need to be set", c.SecretFile) + } + if c.Owner == "" { + return fmt.Errorf("owner('%v')", c.Owner) + } + data, err := c.SecretFile.Read() + if err != nil { + return fmt.Errorf("secretFile('%v') - %w", c.SecretFile, err) + } + c.Secret = string(data) + } + if len(c.AllowedGrantTypes) == 0 { + return fmt.Errorf("allowedGrantTypes('%v') - is empty", c.AllowedGrantTypes) + } + for _, gt := range c.AllowedGrantTypes { + switch gt { + case GrantTypeClientCredentials: + default: + return fmt.Errorf("allowedGrantTypes('%v') - only %v is supported", c.AllowedGrantTypes, GrantTypeClientCredentials) + } + } + if err := c.JWTPrivateKey.Validate(); err != nil { + return fmt.Errorf("privateKeyJWT.%w", err) + } + return nil +} + +type OAuthClientsConfig []*Client + +func (c OAuthClientsConfig) Find(id string) *Client { + for _, client := range c { + if client.ID == id { + return client + } + } + return nil +} + +type Config struct { + PrivateKeyFile urischeme.URIScheme `yaml:"privateKeyFile" json:"privateKeyFile"` + Domain string `yaml:"domain" json:"domain"` + OwnerClaim string `yaml:"ownerClaim" json:"ownerClaim"` + DeviceIDClaim string `yaml:"deviceIDClaim" json:"deviceIDClaim"` + Clients OAuthClientsConfig `yaml:"clients" json:"clients"` +} + +func (c *Config) GetDomain() string { + return "https://" + c.Domain +} + +func (c *Config) GetAuthority() string { + return c.GetDomain() + uri.Base +} + +func (c *Config) Validate() error { + if c.PrivateKeyFile == "" { + return fmt.Errorf("privateKeyFile('%v')", c.PrivateKeyFile) + } + if c.Domain == "" { + return fmt.Errorf("domain('%v')", c.Domain) + } + if len(c.Clients) == 0 { + return fmt.Errorf("clients('%v')", c.Clients) + } + for idx, client := range c.Clients { + if err := client.Validate(); err != nil { + return fmt.Errorf("clients[%v].%w", idx, err) + } + } + return nil +} diff --git a/m2m-oauth-server/service/loadKeys.go b/m2m-oauth-server/oauthSigner/loadKeys.go similarity index 97% rename from m2m-oauth-server/service/loadKeys.go rename to m2m-oauth-server/oauthSigner/loadKeys.go index 683dadf40..876048461 100644 --- a/m2m-oauth-server/service/loadKeys.go +++ b/m2m-oauth-server/oauthSigner/loadKeys.go @@ -1,4 +1,4 @@ -package service +package oauthsigner import ( "crypto/x509" diff --git a/m2m-oauth-server/oauthSigner/oauthSigner.go b/m2m-oauth-server/oauthSigner/oauthSigner.go new file mode 100644 index 000000000..7ad465d16 --- /dev/null +++ b/m2m-oauth-server/oauthSigner/oauthSigner.go @@ -0,0 +1,115 @@ +package oauthsigner + +import ( + "context" + "fmt" + + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/plgd-dev/hub/v2/pkg/fn" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgJwt "github.com/plgd-dev/hub/v2/pkg/security/jwt" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" + "github.com/plgd-dev/kit/v2/codec/json" + "go.opentelemetry.io/otel/trace" +) + +func setKeyError(key string, err error) error { + return fmt.Errorf("failed to set %v: %w", key, err) +} + +type OAuthSigner struct { + privateKeyJWTValidators map[string]*validator.Validator + closer fn.FuncList + config Config + accessTokenKey interface{} + accessTokenJwkKey jwk.Key +} + +func New(ctx context.Context, config Config, getOpenIDConfiguration validator.GetOpenIDConfigurationFunc, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*OAuthSigner, error) { + accessTokenKey, err := LoadPrivateKey(config.PrivateKeyFile) + if err != nil { + return nil, fmt.Errorf("cannot load private privateKeyFile(%v): %w", config.PrivateKeyFile, err) + } + accessTokenJwkKey, err := pkgJwt.CreateJwkKey(accessTokenKey) + if err != nil { + return nil, fmt.Errorf("cannot create jwk for idToken: %w", err) + } + + privateKeyJWTValidators := make(map[string]*validator.Validator, len(config.Clients)) + var closer fn.FuncList + for _, c := range config.Clients { + if !c.JWTPrivateKey.Enabled { + continue + } + validator, err := validator.New(ctx, c.JWTPrivateKey.Authorization, fileWatcher, logger, tracerProvider, validator.WithGetOpenIDConfiguration(getOpenIDConfiguration)) + if err != nil { + closer.Execute() + return nil, fmt.Errorf("cannot create validator: %w", err) + } + privateKeyJWTValidators[c.ID] = validator + closer.AddFunc(validator.Close) + } + return &OAuthSigner{ + privateKeyJWTValidators: privateKeyJWTValidators, + closer: closer, + config: config, + accessTokenKey: accessTokenKey, + accessTokenJwkKey: accessTokenJwkKey, + }, nil +} + +func (s *OAuthSigner) GetValidator(clientID string) (*validator.Validator, bool) { + v, ok := s.privateKeyJWTValidators[clientID] + return v, ok +} + +func (s *OAuthSigner) SignRaw(data []byte) ([]byte, error) { + hdr := jws.NewHeaders() + if err := hdr.Set(jws.TypeKey, `JWT`); err != nil { + return nil, setKeyError(jws.TypeKey, err) + } + if err := hdr.Set(jws.KeyIDKey, s.accessTokenJwkKey.KeyID()); err != nil { + return nil, setKeyError(jws.KeyIDKey, err) + } + + payload, err := jws.Sign(data, jws.WithKey(s.accessTokenJwkKey.Algorithm(), s.accessTokenKey, jws.WithProtectedHeaders(hdr))) + if err != nil { + return nil, fmt.Errorf("failed to create UserToken: %w", err) + } + return payload, nil +} + +func (s *OAuthSigner) GetJWK() jwk.Key { + return s.accessTokenJwkKey +} + +func (s *OAuthSigner) Sign(token jwt.Token) ([]byte, error) { + buf, err := json.Encode(token) + if err != nil { + return nil, fmt.Errorf("failed to encode token: %w", err) + } + return s.SignRaw(buf) +} + +func (s *OAuthSigner) Close() { + s.closer.Execute() +} + +func (s *OAuthSigner) GetDomain() string { + return s.config.GetDomain() +} + +func (s *OAuthSigner) GetOwnerClaim() string { + return s.config.OwnerClaim +} + +func (s *OAuthSigner) GetDeviceIDClaim() string { + return s.config.DeviceIDClaim +} + +func (s *OAuthSigner) GetClients() OAuthClientsConfig { + return s.config.Clients +} diff --git a/m2m-oauth-server/pb/README.md b/m2m-oauth-server/pb/README.md new file mode 100644 index 000000000..dfdac8a60 --- /dev/null +++ b/m2m-oauth-server/pb/README.md @@ -0,0 +1,199 @@ +# Protocol Documentation + + +## Table of Contents + +- [m2m-oauth-server/pb/service.proto](#m2m-oauth-server_pb_service-proto) + - [BlacklistTokensRequest](#m2moauthserver-pb-BlacklistTokensRequest) + - [BlacklistTokensResponse](#m2moauthserver-pb-BlacklistTokensResponse) + - [CreateTokenRequest](#m2moauthserver-pb-CreateTokenRequest) + - [CreateTokenResponse](#m2moauthserver-pb-CreateTokenResponse) + - [GetTokensRequest](#m2moauthserver-pb-GetTokensRequest) + - [Token](#m2moauthserver-pb-Token) + - [Token.BlackListed](#m2moauthserver-pb-Token-BlackListed) + + - [M2MOAuthService](#m2moauthserver-pb-M2MOAuthService) + +- [Scalar Value Types](#scalar-value-types) + + + + +

Top

+ +## m2m-oauth-server/pb/service.proto + + + + + +### BlacklistTokensRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id_filter | [string](#string) | repeated | | + + + + + + + + +### BlacklistTokensResponse + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| count | [int64](#int64) | | | + + + + + + + + +### CreateTokenRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| client_id | [string](#string) | | Client ID | +| client_secret | [string](#string) | | Client Secret | +| audience | [string](#string) | repeated | Requested token Audience | +| scope | [string](#string) | repeated | Requested token scopes | +| expiration | [int64](#int64) | | The requested expiration time in unit timestamp seconds from the client. If not provided, the token will use the maximum allowed by the client, or if it exceeds the maximum allowed, an error will occur. | +| client_assertion_type | [string](#string) | | Client assertion type | +| client_assertion | [string](#string) | | Client assertion | +| token_name | [string](#string) | | Token name | +| grant_type | [string](#string) | | Grant type | + + + + + + + + +### CreateTokenResponse + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| access_token | [string](#string) | | | +| token_type | [string](#string) | | | +| expires_in | [int64](#int64) | | | +| scope | [string](#string) | repeated | | + + + + + + + + +### GetTokensRequest + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id_filter | [string](#string) | repeated | | +| include_blacklisted | [bool](#bool) | | | + + + + + + + + +### Token +Tokens are deleted from DB after they are expired and blacklisted/revoked + +driven by resource change event + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id | [string](#string) | | Token ID / jti | +| version | [uint64](#uint64) | | Incremental version for update | +| name | [string](#string) | | User-friendly token name | +| owner | [string](#string) | | Owner of the token | +| issued_at | [int64](#int64) | | Unix timestamp in s when the condition has been created/updated | +| audience | [string](#string) | repeated | Token Audience | +| scope | [string](#string) | repeated | Token scopes | +| expiration | [int64](#int64) | | Token expiration in Unix timestamp seconds | +| client_id | [string](#string) | | Client ID | +| original_token_claims | [google.protobuf.Value](#google-protobuf-Value) | | Original token claims | +| blacklisted | [Token.BlackListed](#m2moauthserver-pb-Token-BlackListed) | | Token black list section | +| subject | [string](#string) | | Subject of the token | + + + + + + + + +### Token.BlackListed + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| flag | [bool](#bool) | | Blacklisted enabled flag, if once token has been blacklisted then it can't be unblacklisted/unrevoked | +| timestamp | [int64](#int64) | | Unix timestamp in s when the token has been blacklisted | + + + + + + + + + + + + + + +### M2MOAuthService + + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------ | ------------- | ------------| +| CreateToken | [CreateTokenRequest](#m2moauthserver-pb-CreateTokenRequest) | [CreateTokenResponse](#m2moauthserver-pb-CreateTokenResponse) | Creates a new token | +| GetTokens | [GetTokensRequest](#m2moauthserver-pb-GetTokensRequest) | [Token](#m2moauthserver-pb-Token) stream | Returns all tokens of the owner | +| BlacklistTokens | [BlacklistTokensRequest](#m2moauthserver-pb-BlacklistTokensRequest) | [BlacklistTokensResponse](#m2moauthserver-pb-BlacklistTokensResponse) | Blacklists/revokes tokens | + + + + + +## Scalar Value Types + +| .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby | +| ----------- | ----- | --- | ---- | ------ | -- | -- | --- | ---- | +| double | | double | double | float | float64 | double | float | Float | +| float | | float | float | float | float32 | float | float | Float | +| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| uint32 | Uses variable-length encoding. | uint32 | int | int/long | uint32 | uint | integer | Bignum or Fixnum (as required) | +| uint64 | Uses variable-length encoding. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum or Fixnum (as required) | +| sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | uint32 | uint | integer | Bignum or Fixnum (as required) | +| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum | +| sfixed32 | Always four bytes. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| sfixed64 | Always eight bytes. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| bool | | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | +| string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) | +| bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) | + diff --git a/m2m-oauth-server/pb/doc.html b/m2m-oauth-server/pb/doc.html new file mode 100644 index 000000000..a4437f810 --- /dev/null +++ b/m2m-oauth-server/pb/doc.html @@ -0,0 +1,847 @@ + + + + + Protocol Documentation + + + + + + + + + + +

Protocol Documentation

+ +

Table of Contents

+ +
+ +
+ + + +
+

m2m-oauth-server/pb/service.proto

Top +
+

+ + +

BlacklistTokensRequest

+

+ + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
id_filterstringrepeated

+ + + + + +

BlacklistTokensResponse

+

+ + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
countint64

+ + + + + +

CreateTokenRequest

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
client_idstring

Client ID

client_secretstring

Client Secret

audiencestringrepeated

Requested token Audience

scopestringrepeated

Requested token scopes

expirationint64

The requested expiration time in unit timestamp seconds from the client. If not provided, the token will use the maximum allowed by the client, or if it exceeds the maximum allowed, an error will occur.

client_assertion_typestring

Client assertion type

client_assertionstring

Client assertion

token_namestring

Token name

grant_typestring

Grant type

+ + + + + +

CreateTokenResponse

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
access_tokenstring

token_typestring

expires_inint64

scopestringrepeated

+ + + + + +

GetTokensRequest

+

+ + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
id_filterstringrepeated

include_blacklistedbool

+ + + + + +

Token

+

Tokens are deleted from DB after they are expired and blacklisted/revoked

driven by resource change event

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
idstring

Token ID / jti

versionuint64

Incremental version for update

namestring

User-friendly token name

ownerstring

Owner of the token

issued_atint64

Unix timestamp in s when the condition has been created/updated

audiencestringrepeated

Token Audience

scopestringrepeated

Token scopes

expirationint64

Token expiration in Unix timestamp seconds

client_idstring

Client ID

original_token_claimsgoogle.protobuf.Value

Original token claims

blacklistedToken.BlackListed

Token black list section

subjectstring

Subject of the token

+ + + + + +

Token.BlackListed

+

+ + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeLabelDescription
flagbool

Blacklisted enabled flag, if once token has been blacklisted then it can't be unblacklisted/unrevoked

timestampint64

Unix timestamp in s when the token has been blacklisted

+ + + + + + + + + + + +

M2MOAuthService

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Method NameRequest TypeResponse TypeDescription
CreateTokenCreateTokenRequestCreateTokenResponse

Creates a new token

GetTokensGetTokensRequestToken stream

Returns all tokens of the owner

BlacklistTokensBlacklistTokensRequestBlacklistTokensResponse

Blacklists/revokes tokens

+ + + + +

Methods with HTTP bindings

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Method NameMethodPatternBody
CreateTokenPOST/m2m-oauth-server/api/v1/tokens*
GetTokensGET/m2m-oauth-server/api/v1/tokens
BlacklistTokensPOST/m2m-oauth-server/api/v1/blacklist*
+ + + + +

Scalar Value Types

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
.proto TypeNotesC++JavaPythonGoC#PHPRuby
doubledoubledoublefloatfloat64doublefloatFloat
floatfloatfloatfloatfloat32floatfloatFloat
int32Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.int32intintint32intintegerBignum or Fixnum (as required)
int64Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.int64longint/longint64longinteger/stringBignum
uint32Uses variable-length encoding.uint32intint/longuint32uintintegerBignum or Fixnum (as required)
uint64Uses variable-length encoding.uint64longint/longuint64ulonginteger/stringBignum or Fixnum (as required)
sint32Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.int32intintint32intintegerBignum or Fixnum (as required)
sint64Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.int64longint/longint64longinteger/stringBignum
fixed32Always four bytes. More efficient than uint32 if values are often greater than 2^28.uint32intintuint32uintintegerBignum or Fixnum (as required)
fixed64Always eight bytes. More efficient than uint64 if values are often greater than 2^56.uint64longint/longuint64ulonginteger/stringBignum
sfixed32Always four bytes.int32intintint32intintegerBignum or Fixnum (as required)
sfixed64Always eight bytes.int64longint/longint64longinteger/stringBignum
boolboolbooleanbooleanboolboolbooleanTrueClass/FalseClass
stringA string must always contain UTF-8 encoded or 7-bit ASCII text.stringStringstr/unicodestringstringstringString (UTF-8)
bytesMay contain any arbitrary sequence of bytes.stringByteStringstr[]byteByteStringstringString (ASCII-8BIT)
+ + + diff --git a/m2m-oauth-server/pb/service.pb.go b/m2m-oauth-server/pb/service.pb.go new file mode 100644 index 000000000..63f911c3b --- /dev/null +++ b/m2m-oauth-server/pb/service.pb.go @@ -0,0 +1,857 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.1 +// protoc v5.26.1 +// source: m2m-oauth-server/pb/service.proto + +package pb + +import ( + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Tokens are deleted from DB after they are expired and blacklisted/revoked +type Token struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Token ID / jti + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Incremental version for update + Version uint64 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + // User-friendly token name + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + // Owner of the token + Owner string `protobuf:"bytes,4,opt,name=owner,proto3" json:"owner,omitempty"` + // Unix timestamp in s when the condition has been created/updated + IssuedAt int64 `protobuf:"varint,5,opt,name=issued_at,json=issuedAt,proto3" json:"issued_at,omitempty"` + // Token Audience + Audience []string `protobuf:"bytes,6,rep,name=audience,proto3" json:"audience,omitempty"` + // Token scopes + Scope []string `protobuf:"bytes,7,rep,name=scope,proto3" json:"scope,omitempty"` + // Token expiration in Unix timestamp seconds + Expiration int64 `protobuf:"varint,8,opt,name=expiration,proto3" json:"expiration,omitempty"` + // Client ID + ClientId string `protobuf:"bytes,9,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + // Original token claims + OriginalTokenClaims *structpb.Value `protobuf:"bytes,10,opt,name=original_token_claims,json=originalTokenClaims,proto3" json:"original_token_claims,omitempty"` + // Token black list section + Blacklisted *Token_BlackListed `protobuf:"bytes,11,opt,name=blacklisted,proto3" json:"blacklisted,omitempty"` + // Subject of the token + Subject string `protobuf:"bytes,12,opt,name=subject,proto3" json:"subject,omitempty"` +} + +func (x *Token) Reset() { + *x = Token{} + if protoimpl.UnsafeEnabled { + mi := &file_m2m_oauth_server_pb_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Token) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Token) ProtoMessage() {} + +func (x *Token) ProtoReflect() protoreflect.Message { + mi := &file_m2m_oauth_server_pb_service_proto_msgTypes[0] + 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 Token.ProtoReflect.Descriptor instead. +func (*Token) Descriptor() ([]byte, []int) { + return file_m2m_oauth_server_pb_service_proto_rawDescGZIP(), []int{0} +} + +func (x *Token) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Token) GetVersion() uint64 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Token) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Token) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *Token) GetIssuedAt() int64 { + if x != nil { + return x.IssuedAt + } + return 0 +} + +func (x *Token) GetAudience() []string { + if x != nil { + return x.Audience + } + return nil +} + +func (x *Token) GetScope() []string { + if x != nil { + return x.Scope + } + return nil +} + +func (x *Token) GetExpiration() int64 { + if x != nil { + return x.Expiration + } + return 0 +} + +func (x *Token) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *Token) GetOriginalTokenClaims() *structpb.Value { + if x != nil { + return x.OriginalTokenClaims + } + return nil +} + +func (x *Token) GetBlacklisted() *Token_BlackListed { + if x != nil { + return x.Blacklisted + } + return nil +} + +func (x *Token) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +type GetTokensRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IdFilter []string `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` + IncludeBlacklisted bool `protobuf:"varint,3,opt,name=include_blacklisted,json=includeBlacklisted,proto3" json:"include_blacklisted,omitempty"` +} + +func (x *GetTokensRequest) Reset() { + *x = GetTokensRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_m2m_oauth_server_pb_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetTokensRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTokensRequest) ProtoMessage() {} + +func (x *GetTokensRequest) ProtoReflect() protoreflect.Message { + mi := &file_m2m_oauth_server_pb_service_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 GetTokensRequest.ProtoReflect.Descriptor instead. +func (*GetTokensRequest) Descriptor() ([]byte, []int) { + return file_m2m_oauth_server_pb_service_proto_rawDescGZIP(), []int{1} +} + +func (x *GetTokensRequest) GetIdFilter() []string { + if x != nil { + return x.IdFilter + } + return nil +} + +func (x *GetTokensRequest) GetIncludeBlacklisted() bool { + if x != nil { + return x.IncludeBlacklisted + } + return false +} + +type BlacklistTokensRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IdFilter []string `protobuf:"bytes,1,rep,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"` +} + +func (x *BlacklistTokensRequest) Reset() { + *x = BlacklistTokensRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_m2m_oauth_server_pb_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BlacklistTokensRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlacklistTokensRequest) ProtoMessage() {} + +func (x *BlacklistTokensRequest) ProtoReflect() protoreflect.Message { + mi := &file_m2m_oauth_server_pb_service_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 BlacklistTokensRequest.ProtoReflect.Descriptor instead. +func (*BlacklistTokensRequest) Descriptor() ([]byte, []int) { + return file_m2m_oauth_server_pb_service_proto_rawDescGZIP(), []int{2} +} + +func (x *BlacklistTokensRequest) GetIdFilter() []string { + if x != nil { + return x.IdFilter + } + return nil +} + +type BlacklistTokensResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` +} + +func (x *BlacklistTokensResponse) Reset() { + *x = BlacklistTokensResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_m2m_oauth_server_pb_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BlacklistTokensResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlacklistTokensResponse) ProtoMessage() {} + +func (x *BlacklistTokensResponse) ProtoReflect() protoreflect.Message { + mi := &file_m2m_oauth_server_pb_service_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 BlacklistTokensResponse.ProtoReflect.Descriptor instead. +func (*BlacklistTokensResponse) Descriptor() ([]byte, []int) { + return file_m2m_oauth_server_pb_service_proto_rawDescGZIP(), []int{3} +} + +func (x *BlacklistTokensResponse) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +type CreateTokenRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Client ID + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + // Client Secret + ClientSecret string `protobuf:"bytes,2,opt,name=client_secret,json=clientSecret,proto3" json:"client_secret,omitempty"` + // Requested token Audience + Audience []string `protobuf:"bytes,3,rep,name=audience,proto3" json:"audience,omitempty"` + // Requested token scopes + Scope []string `protobuf:"bytes,4,rep,name=scope,proto3" json:"scope,omitempty"` + // The requested expiration time in unit timestamp seconds from the client. If not provided, the token will use the maximum allowed by the client, or if it exceeds the maximum allowed, an error will occur. + Expiration int64 `protobuf:"varint,5,opt,name=expiration,proto3" json:"expiration,omitempty"` + // Client assertion type + ClientAssertionType string `protobuf:"bytes,6,opt,name=client_assertion_type,json=clientAssertionType,proto3" json:"client_assertion_type,omitempty"` + // Client assertion + ClientAssertion string `protobuf:"bytes,7,opt,name=client_assertion,json=clientAssertion,proto3" json:"client_assertion,omitempty"` + // Token name + TokenName string `protobuf:"bytes,8,opt,name=token_name,json=tokenName,proto3" json:"token_name,omitempty"` + // Grant type + GrantType string `protobuf:"bytes,9,opt,name=grant_type,json=grantType,proto3" json:"grant_type,omitempty"` +} + +func (x *CreateTokenRequest) Reset() { + *x = CreateTokenRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_m2m_oauth_server_pb_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTokenRequest) ProtoMessage() {} + +func (x *CreateTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_m2m_oauth_server_pb_service_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 CreateTokenRequest.ProtoReflect.Descriptor instead. +func (*CreateTokenRequest) Descriptor() ([]byte, []int) { + return file_m2m_oauth_server_pb_service_proto_rawDescGZIP(), []int{4} +} + +func (x *CreateTokenRequest) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *CreateTokenRequest) GetClientSecret() string { + if x != nil { + return x.ClientSecret + } + return "" +} + +func (x *CreateTokenRequest) GetAudience() []string { + if x != nil { + return x.Audience + } + return nil +} + +func (x *CreateTokenRequest) GetScope() []string { + if x != nil { + return x.Scope + } + return nil +} + +func (x *CreateTokenRequest) GetExpiration() int64 { + if x != nil { + return x.Expiration + } + return 0 +} + +func (x *CreateTokenRequest) GetClientAssertionType() string { + if x != nil { + return x.ClientAssertionType + } + return "" +} + +func (x *CreateTokenRequest) GetClientAssertion() string { + if x != nil { + return x.ClientAssertion + } + return "" +} + +func (x *CreateTokenRequest) GetTokenName() string { + if x != nil { + return x.TokenName + } + return "" +} + +func (x *CreateTokenRequest) GetGrantType() string { + if x != nil { + return x.GrantType + } + return "" +} + +type CreateTokenResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + TokenType string `protobuf:"bytes,2,opt,name=token_type,json=tokenType,proto3" json:"token_type,omitempty"` + ExpiresIn int64 `protobuf:"varint,3,opt,name=expires_in,json=expiresIn,proto3" json:"expires_in,omitempty"` + Scope []string `protobuf:"bytes,4,rep,name=scope,proto3" json:"scope,omitempty"` +} + +func (x *CreateTokenResponse) Reset() { + *x = CreateTokenResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_m2m_oauth_server_pb_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTokenResponse) ProtoMessage() {} + +func (x *CreateTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_m2m_oauth_server_pb_service_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 CreateTokenResponse.ProtoReflect.Descriptor instead. +func (*CreateTokenResponse) Descriptor() ([]byte, []int) { + return file_m2m_oauth_server_pb_service_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateTokenResponse) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *CreateTokenResponse) GetTokenType() string { + if x != nil { + return x.TokenType + } + return "" +} + +func (x *CreateTokenResponse) GetExpiresIn() int64 { + if x != nil { + return x.ExpiresIn + } + return 0 +} + +func (x *CreateTokenResponse) GetScope() []string { + if x != nil { + return x.Scope + } + return nil +} + +type Token_BlackListed struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Blacklisted enabled flag, if once token has been blacklisted then it can't be unblacklisted/unrevoked + Flag bool `protobuf:"varint,1,opt,name=flag,proto3" json:"flag,omitempty"` + // Unix timestamp in s when the token has been blacklisted + Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` +} + +func (x *Token_BlackListed) Reset() { + *x = Token_BlackListed{} + if protoimpl.UnsafeEnabled { + mi := &file_m2m_oauth_server_pb_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Token_BlackListed) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Token_BlackListed) ProtoMessage() {} + +func (x *Token_BlackListed) ProtoReflect() protoreflect.Message { + mi := &file_m2m_oauth_server_pb_service_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 Token_BlackListed.ProtoReflect.Descriptor instead. +func (*Token_BlackListed) Descriptor() ([]byte, []int) { + return file_m2m_oauth_server_pb_service_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *Token_BlackListed) GetFlag() bool { + if x != nil { + return x.Flag + } + return false +} + +func (x *Token_BlackListed) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +var File_m2m_oauth_server_pb_service_proto protoreflect.FileDescriptor + +var file_m2m_oauth_server_pb_service_proto_rawDesc = []byte{ + 0x0a, 0x21, 0x6d, 0x32, 0x6d, 0x2d, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x2f, 0x70, 0x62, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x11, 0x6d, 0x32, 0x6d, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x2e, 0x70, 0x62, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 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, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, + 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0xd6, 0x03, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, + 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, + 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1a, 0x0a, + 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, + 0x1e, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x4a, 0x0a, 0x15, + 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x63, + 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x13, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x12, 0x46, 0x0a, 0x0b, 0x62, 0x6c, 0x61, 0x63, + 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, + 0x6d, 0x32, 0x6d, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x70, + 0x62, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x42, 0x6c, 0x61, 0x63, 0x6b, 0x4c, 0x69, 0x73, + 0x74, 0x65, 0x64, 0x52, 0x0b, 0x62, 0x6c, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x64, + 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x1a, 0x3f, 0x0a, 0x0b, 0x42, 0x6c, + 0x61, 0x63, 0x6b, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x61, + 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x66, 0x6c, 0x61, 0x67, 0x12, 0x1c, 0x0a, + 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x60, 0x0a, 0x10, 0x47, + 0x65, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1b, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x2f, 0x0a, 0x13, + 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x62, 0x6c, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, + 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x69, 0x6e, 0x63, 0x6c, 0x75, + 0x64, 0x65, 0x42, 0x6c, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x64, 0x22, 0x35, 0x0a, + 0x16, 0x42, 0x6c, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x22, 0x2f, 0x0a, 0x17, 0x42, 0x6c, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, + 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xc5, 0x02, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1a, + 0x0a, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, + 0x6f, 0x70, 0x65, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x72, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x13, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, 0x73, 0x73, 0x65, 0x72, 0x74, 0x69, 0x6f, 0x6e, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x61, + 0x73, 0x73, 0x65, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, 0x73, 0x73, 0x65, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, + 0x0a, 0x0a, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x22, 0x8c, 0x01, + 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 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, 0x1d, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x65, 0x73, 0x5f, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x65, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x73, 0x49, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x32, 0xcf, 0x03, 0x0a, + 0x0f, 0x4d, 0x32, 0x4d, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x93, 0x01, 0x0a, 0x0b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x12, 0x25, 0x2e, 0x6d, 0x32, 0x6d, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6d, 0x32, 0x6d, 0x6f, 0x61, 0x75, + 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x35, 0x92, 0x41, 0x08, 0x0a, 0x06, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x24, 0x3a, 0x01, 0x2a, 0x22, 0x1f, 0x2f, 0x6d, 0x32, 0x6d, 0x2d, 0x6f, 0x61, 0x75, 0x74, + 0x68, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x80, 0x01, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x23, 0x2e, 0x6d, 0x32, 0x6d, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x70, 0x62, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6d, 0x32, 0x6d, 0x6f, + 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x22, 0x32, 0x92, 0x41, 0x08, 0x0a, 0x06, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x12, 0x1f, 0x2f, 0x6d, 0x32, 0x6d, 0x2d, 0x6f, 0x61, 0x75, + 0x74, 0x68, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x2f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x30, 0x01, 0x12, 0xa2, 0x01, 0x0a, 0x0f, 0x42, 0x6c, + 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x29, 0x2e, + 0x6d, 0x32, 0x6d, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x70, + 0x62, 0x2e, 0x42, 0x6c, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6d, 0x32, 0x6d, 0x6f, 0x61, + 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x70, 0x62, 0x2e, 0x42, 0x6c, 0x61, + 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x38, 0x92, 0x41, 0x08, 0x0a, 0x06, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x73, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x27, 0x3a, 0x01, 0x2a, 0x22, 0x22, 0x2f, 0x6d, 0x32, 0x6d, + 0x2d, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x6c, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x42, 0xcb, + 0x02, 0x92, 0x41, 0x94, 0x02, 0x12, 0xbc, 0x01, 0x0a, 0x0c, 0x50, 0x4c, 0x47, 0x44, 0x20, 0x4d, + 0x32, 0x4d, 0x20, 0x41, 0x50, 0x49, 0x12, 0x24, 0x41, 0x50, 0x49, 0x20, 0x66, 0x6f, 0x72, 0x20, + 0x74, 0x6f, 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x20, 0x6d, 0x32, 0x6d, 0x20, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x50, 0x4c, 0x47, 0x44, 0x22, 0x3a, 0x0a, 0x08, + 0x70, 0x6c, 0x67, 0x64, 0x2e, 0x64, 0x65, 0x76, 0x12, 0x1f, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, + 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, + 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x1a, 0x0d, 0x69, 0x6e, 0x66, 0x6f, 0x40, + 0x70, 0x6c, 0x67, 0x64, 0x2e, 0x64, 0x65, 0x76, 0x2a, 0x45, 0x0a, 0x12, 0x41, 0x70, 0x61, 0x63, + 0x68, 0x65, 0x20, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x20, 0x32, 0x2e, 0x30, 0x12, 0x2f, + 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, + 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, + 0x03, 0x31, 0x2e, 0x30, 0x2a, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x32, 0x15, 0x61, 0x70, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6a, 0x73, 0x6f, 0x6e, + 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, + 0x6f, 0x6e, 0x3a, 0x15, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6a, 0x73, 0x6f, 0x6e, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, + 0x75, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x6d, 0x32, 0x6d, 0x2d, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x2d, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_m2m_oauth_server_pb_service_proto_rawDescOnce sync.Once + file_m2m_oauth_server_pb_service_proto_rawDescData = file_m2m_oauth_server_pb_service_proto_rawDesc +) + +func file_m2m_oauth_server_pb_service_proto_rawDescGZIP() []byte { + file_m2m_oauth_server_pb_service_proto_rawDescOnce.Do(func() { + file_m2m_oauth_server_pb_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_m2m_oauth_server_pb_service_proto_rawDescData) + }) + return file_m2m_oauth_server_pb_service_proto_rawDescData +} + +var file_m2m_oauth_server_pb_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_m2m_oauth_server_pb_service_proto_goTypes = []interface{}{ + (*Token)(nil), // 0: m2moauthserver.pb.Token + (*GetTokensRequest)(nil), // 1: m2moauthserver.pb.GetTokensRequest + (*BlacklistTokensRequest)(nil), // 2: m2moauthserver.pb.BlacklistTokensRequest + (*BlacklistTokensResponse)(nil), // 3: m2moauthserver.pb.BlacklistTokensResponse + (*CreateTokenRequest)(nil), // 4: m2moauthserver.pb.CreateTokenRequest + (*CreateTokenResponse)(nil), // 5: m2moauthserver.pb.CreateTokenResponse + (*Token_BlackListed)(nil), // 6: m2moauthserver.pb.Token.BlackListed + (*structpb.Value)(nil), // 7: google.protobuf.Value +} +var file_m2m_oauth_server_pb_service_proto_depIdxs = []int32{ + 7, // 0: m2moauthserver.pb.Token.original_token_claims:type_name -> google.protobuf.Value + 6, // 1: m2moauthserver.pb.Token.blacklisted:type_name -> m2moauthserver.pb.Token.BlackListed + 4, // 2: m2moauthserver.pb.M2MOAuthService.CreateToken:input_type -> m2moauthserver.pb.CreateTokenRequest + 1, // 3: m2moauthserver.pb.M2MOAuthService.GetTokens:input_type -> m2moauthserver.pb.GetTokensRequest + 2, // 4: m2moauthserver.pb.M2MOAuthService.BlacklistTokens:input_type -> m2moauthserver.pb.BlacklistTokensRequest + 5, // 5: m2moauthserver.pb.M2MOAuthService.CreateToken:output_type -> m2moauthserver.pb.CreateTokenResponse + 0, // 6: m2moauthserver.pb.M2MOAuthService.GetTokens:output_type -> m2moauthserver.pb.Token + 3, // 7: m2moauthserver.pb.M2MOAuthService.BlacklistTokens:output_type -> m2moauthserver.pb.BlacklistTokensResponse + 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 +} + +func init() { file_m2m_oauth_server_pb_service_proto_init() } +func file_m2m_oauth_server_pb_service_proto_init() { + if File_m2m_oauth_server_pb_service_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_m2m_oauth_server_pb_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Token); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_m2m_oauth_server_pb_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetTokensRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_m2m_oauth_server_pb_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BlacklistTokensRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_m2m_oauth_server_pb_service_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BlacklistTokensResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_m2m_oauth_server_pb_service_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateTokenRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_m2m_oauth_server_pb_service_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateTokenResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_m2m_oauth_server_pb_service_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Token_BlackListed); 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{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_m2m_oauth_server_pb_service_proto_rawDesc, + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_m2m_oauth_server_pb_service_proto_goTypes, + DependencyIndexes: file_m2m_oauth_server_pb_service_proto_depIdxs, + MessageInfos: file_m2m_oauth_server_pb_service_proto_msgTypes, + }.Build() + File_m2m_oauth_server_pb_service_proto = out.File + file_m2m_oauth_server_pb_service_proto_rawDesc = nil + file_m2m_oauth_server_pb_service_proto_goTypes = nil + file_m2m_oauth_server_pb_service_proto_depIdxs = nil +} diff --git a/m2m-oauth-server/pb/service.pb.gw.go b/m2m-oauth-server/pb/service.pb.gw.go new file mode 100644 index 000000000..aa6e8b730 --- /dev/null +++ b/m2m-oauth-server/pb/service.pb.gw.go @@ -0,0 +1,301 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: m2m-oauth-server/pb/service.proto + +/* +Package pb is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package pb + +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_M2MOAuthService_CreateToken_0(ctx context.Context, marshaler runtime.Marshaler, client M2MOAuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CreateTokenRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.CreateToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_M2MOAuthService_CreateToken_0(ctx context.Context, marshaler runtime.Marshaler, server M2MOAuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CreateTokenRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.CreateToken(ctx, &protoReq) + return msg, metadata, err + +} + +var ( + filter_M2MOAuthService_GetTokens_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_M2MOAuthService_GetTokens_0(ctx context.Context, marshaler runtime.Marshaler, client M2MOAuthServiceClient, req *http.Request, pathParams map[string]string) (M2MOAuthService_GetTokensClient, runtime.ServerMetadata, error) { + var protoReq GetTokensRequest + 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_M2MOAuthService_GetTokens_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.GetTokens(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +func request_M2MOAuthService_BlacklistTokens_0(ctx context.Context, marshaler runtime.Marshaler, client M2MOAuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq BlacklistTokensRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.BlacklistTokens(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_M2MOAuthService_BlacklistTokens_0(ctx context.Context, marshaler runtime.Marshaler, server M2MOAuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq BlacklistTokensRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.BlacklistTokens(ctx, &protoReq) + return msg, metadata, err + +} + +// RegisterM2MOAuthServiceHandlerServer registers the http handlers for service M2MOAuthService to "mux". +// UnaryRPC :call M2MOAuthServiceServer 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 RegisterM2MOAuthServiceHandlerFromEndpoint instead. +func RegisterM2MOAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server M2MOAuthServiceServer) error { + + mux.Handle("POST", pattern_M2MOAuthService_CreateToken_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) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/m2moauthserver.pb.M2MOAuthService/CreateToken", runtime.WithHTTPPathPattern("/m2m-oauth-server/api/v1/tokens")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_M2MOAuthService_CreateToken_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_M2MOAuthService_CreateToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_M2MOAuthService_GetTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + mux.Handle("POST", pattern_M2MOAuthService_BlacklistTokens_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) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/m2moauthserver.pb.M2MOAuthService/BlacklistTokens", runtime.WithHTTPPathPattern("/m2m-oauth-server/api/v1/blacklist")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_M2MOAuthService_BlacklistTokens_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_M2MOAuthService_BlacklistTokens_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +// RegisterM2MOAuthServiceHandlerFromEndpoint is same as RegisterM2MOAuthServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterM2MOAuthServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterM2MOAuthServiceHandler(ctx, mux, conn) +} + +// RegisterM2MOAuthServiceHandler registers the http handlers for service M2MOAuthService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterM2MOAuthServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterM2MOAuthServiceHandlerClient(ctx, mux, NewM2MOAuthServiceClient(conn)) +} + +// RegisterM2MOAuthServiceHandlerClient registers the http handlers for service M2MOAuthService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "M2MOAuthServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "M2MOAuthServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "M2MOAuthServiceClient" to call the correct interceptors. +func RegisterM2MOAuthServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client M2MOAuthServiceClient) error { + + mux.Handle("POST", pattern_M2MOAuthService_CreateToken_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) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/m2moauthserver.pb.M2MOAuthService/CreateToken", runtime.WithHTTPPathPattern("/m2m-oauth-server/api/v1/tokens")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_M2MOAuthService_CreateToken_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_M2MOAuthService_CreateToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_M2MOAuthService_GetTokens_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) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/m2moauthserver.pb.M2MOAuthService/GetTokens", runtime.WithHTTPPathPattern("/m2m-oauth-server/api/v1/tokens")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_M2MOAuthService_GetTokens_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_M2MOAuthService_GetTokens_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_M2MOAuthService_BlacklistTokens_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) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/m2moauthserver.pb.M2MOAuthService/BlacklistTokens", runtime.WithHTTPPathPattern("/m2m-oauth-server/api/v1/blacklist")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_M2MOAuthService_BlacklistTokens_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_M2MOAuthService_BlacklistTokens_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_M2MOAuthService_CreateToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"m2m-oauth-server", "api", "v1", "tokens"}, "")) + + pattern_M2MOAuthService_GetTokens_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"m2m-oauth-server", "api", "v1", "tokens"}, "")) + + pattern_M2MOAuthService_BlacklistTokens_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"m2m-oauth-server", "api", "v1", "blacklist"}, "")) +) + +var ( + forward_M2MOAuthService_CreateToken_0 = runtime.ForwardResponseMessage + + forward_M2MOAuthService_GetTokens_0 = runtime.ForwardResponseStream + + forward_M2MOAuthService_BlacklistTokens_0 = runtime.ForwardResponseMessage +) diff --git a/m2m-oauth-server/pb/service.proto b/m2m-oauth-server/pb/service.proto new file mode 100644 index 000000000..a55f56b4a --- /dev/null +++ b/m2m-oauth-server/pb/service.proto @@ -0,0 +1,140 @@ +syntax = "proto3"; + +package m2moauthserver.pb; + +import "google/protobuf/struct.proto"; +import "google/api/annotations.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "PLGD M2M API"; + version: "1.0"; + description: "API for to manage m2m tokens in PLGD"; + contact: { + name: "plgd.dev"; + url: "https://github.com/plgd-dev/hub"; + email: "info@plgd.dev"; + }; + license: { + name: "Apache License 2.0"; + url: "https://github.com/plgd-dev/hub/blob/v2/LICENSE"; + }; + }; + schemes: [HTTPS]; + consumes: ["application/json", "application/protojson"]; + produces: ["application/json", "application/protojson"]; +}; + +option go_package = "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb;pb"; + +// Tokens are deleted from DB after they are expired and blacklisted/revoked +message Token { // driven by resource change event + // Token ID / jti + string id = 1; + // Incremental version for update + uint64 version = 2; + // User-friendly token name + string name = 3; + // Owner of the token + string owner = 4; + // Unix timestamp in s when the condition has been created/updated + int64 issued_at = 5; + // Token Audience + repeated string audience = 6; + // Token scopes + repeated string scope = 7; + // Token expiration in Unix timestamp seconds + int64 expiration = 8; + // Client ID + string client_id = 9; + // Original token claims + google.protobuf.Value original_token_claims = 10; + message BlackListed { + // Blacklisted enabled flag, if once token has been blacklisted then it can't be unblacklisted/unrevoked + bool flag = 1; + // Unix timestamp in s when the token has been blacklisted + int64 timestamp = 2; + } + // Token black list section + BlackListed blacklisted = 11; + // Subject of the token + string subject = 12; +} + + + +message GetTokensRequest { + repeated string id_filter = 1; + bool include_blacklisted = 2; +} + +message BlacklistTokensRequest { + repeated string id_filter = 1; +} + +message BlacklistTokensResponse { + int64 count = 1; +} + +message CreateTokenRequest { + // Client ID + string client_id = 1; + // Client Secret + string client_secret = 2; + // Requested token Audience + repeated string audience = 3; + // Requested token scopes + repeated string scope = 4; + // The requested expiration time in unit timestamp seconds from the client. If not provided, the token will use the maximum allowed by the client, or if it exceeds the maximum allowed, an error will occur. + int64 expiration = 5; + // Client assertion type + string client_assertion_type = 6; + // Client assertion + string client_assertion = 7; + // Token name + string token_name = 8; + // Grant type + string grant_type = 9; +} + +message CreateTokenResponse { + string access_token = 1; + string token_type = 2; + int64 expires_in = 3; + repeated string scope = 4; +} + +service M2MOAuthService { + // Creates a new token + rpc CreateToken(CreateTokenRequest) returns (CreateTokenResponse) { + option (google.api.http) = { + post: "/m2m-oauth-server/api/v1/tokens"; + body: "*"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Tokens" ]; + }; + } + + // Returns all tokens of the owner + rpc GetTokens(GetTokensRequest) returns (stream Token) { + option (google.api.http) = { + get: "/m2m-oauth-server/api/v1/tokens"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Tokens" ]; + }; + } + + // Blacklists/revokes tokens + rpc BlacklistTokens(BlacklistTokensRequest) returns (BlacklistTokensResponse) { + option (google.api.http) = { + post: "/m2m-oauth-server/api/v1/blacklist"; + body: "*"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: [ "Tokens" ]; + }; + } +} diff --git a/m2m-oauth-server/pb/service.swagger.json b/m2m-oauth-server/pb/service.swagger.json new file mode 100644 index 000000000..46abc4c85 --- /dev/null +++ b/m2m-oauth-server/pb/service.swagger.json @@ -0,0 +1,353 @@ +{ + "swagger": "2.0", + "info": { + "title": "PLGD M2M API", + "description": "API for to manage m2m tokens in PLGD", + "version": "1.0", + "contact": { + "name": "plgd.dev", + "url": "https://github.com/plgd-dev/hub", + "email": "info@plgd.dev" + }, + "license": { + "name": "Apache License 2.0", + "url": "https://github.com/plgd-dev/hub/blob/v2/LICENSE" + } + }, + "tags": [ + { + "name": "M2MOAuthService" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json", + "application/protojson" + ], + "produces": [ + "application/json", + "application/protojson" + ], + "paths": { + "/m2m-oauth-server/api/v1/blacklist": { + "post": { + "summary": "Blacklists/revokes tokens", + "operationId": "M2MOAuthService_BlacklistTokens", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbBlacklistTokensResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pbBlacklistTokensRequest" + } + } + ], + "tags": [ + "Tokens" + ] + } + }, + "/m2m-oauth-server/api/v1/tokens": { + "get": { + "summary": "Returns all tokens of the owner", + "operationId": "M2MOAuthService_GetTokens", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/pbToken" + }, + "error": { + "$ref": "#/definitions/rpcStatus" + } + }, + "title": "Stream result of pbToken" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "idFilter", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "includeBlacklisted", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "tags": [ + "Tokens" + ] + }, + "post": { + "summary": "Creates a new token", + "operationId": "M2MOAuthService_CreateToken", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pbCreateTokenResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pbCreateTokenRequest" + } + } + ], + "tags": [ + "Tokens" + ] + } + } + }, + "definitions": { + "TokenBlackListed": { + "type": "object", + "properties": { + "flag": { + "type": "boolean", + "title": "Blacklisted enabled flag, if once token has been blacklisted then it can't be unblacklisted/unrevoked" + }, + "timestamp": { + "type": "string", + "format": "int64", + "title": "Unix timestamp in s when the token has been blacklisted" + } + } + }, + "pbBlacklistTokensRequest": { + "type": "object", + "properties": { + "idFilter": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "pbBlacklistTokensResponse": { + "type": "object", + "properties": { + "count": { + "type": "string", + "format": "int64" + } + } + }, + "pbCreateTokenRequest": { + "type": "object", + "properties": { + "clientId": { + "type": "string", + "title": "Client ID" + }, + "clientSecret": { + "type": "string", + "title": "Client Secret" + }, + "audience": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Requested token Audience" + }, + "scope": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Requested token scopes" + }, + "expiration": { + "type": "string", + "format": "int64", + "description": "The requested expiration time in unit timestamp seconds from the client. If not provided, the token will use the maximum allowed by the client, or if it exceeds the maximum allowed, an error will occur." + }, + "clientAssertionType": { + "type": "string", + "title": "Client assertion type" + }, + "clientAssertion": { + "type": "string", + "title": "Client assertion" + }, + "tokenName": { + "type": "string", + "title": "Token name" + }, + "grantType": { + "type": "string", + "title": "Grant type" + } + } + }, + "pbCreateTokenResponse": { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "tokenType": { + "type": "string" + }, + "expiresIn": { + "type": "string", + "format": "int64" + }, + "scope": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "pbToken": { + "type": "object", + "properties": { + "id": { + "type": "string", + "title": "Token ID / jti" + }, + "version": { + "type": "string", + "format": "uint64", + "title": "Incremental version for update" + }, + "name": { + "type": "string", + "title": "User-friendly token name" + }, + "owner": { + "type": "string", + "title": "Owner of the token" + }, + "issuedAt": { + "type": "string", + "format": "int64", + "title": "Unix timestamp in s when the condition has been created/updated" + }, + "audience": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Token Audience" + }, + "scope": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Token scopes" + }, + "expiration": { + "type": "string", + "format": "int64", + "title": "Token expiration in Unix timestamp seconds" + }, + "clientId": { + "type": "string", + "title": "Client ID" + }, + "originalTokenClaims": { + "title": "Original token claims" + }, + "blacklisted": { + "$ref": "#/definitions/TokenBlackListed", + "title": "Token black list section" + }, + "subject": { + "type": "string", + "title": "Subject of the token" + } + }, + "description": "driven by resource change event", + "title": "Tokens are deleted from DB after they are expired and blacklisted/revoked" + }, + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "protobufNullValue": { + "type": "string", + "enum": [ + "NULL_VALUE" + ], + "default": "NULL_VALUE", + "description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\nThe JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value." + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + } + } +} diff --git a/m2m-oauth-server/pb/service_grpc.pb.go b/m2m-oauth-server/pb/service_grpc.pb.go new file mode 100644 index 000000000..900be79a9 --- /dev/null +++ b/m2m-oauth-server/pb/service_grpc.pb.go @@ -0,0 +1,217 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v5.26.1 +// source: m2m-oauth-server/pb/service.proto + +package pb + +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 + +const ( + M2MOAuthService_CreateToken_FullMethodName = "/m2moauthserver.pb.M2MOAuthService/CreateToken" + M2MOAuthService_GetTokens_FullMethodName = "/m2moauthserver.pb.M2MOAuthService/GetTokens" + M2MOAuthService_BlacklistTokens_FullMethodName = "/m2moauthserver.pb.M2MOAuthService/BlacklistTokens" +) + +// M2MOAuthServiceClient is the client API for M2MOAuthService 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 M2MOAuthServiceClient interface { + // Creates a new token + CreateToken(ctx context.Context, in *CreateTokenRequest, opts ...grpc.CallOption) (*CreateTokenResponse, error) + // Returns all tokens of the owner + GetTokens(ctx context.Context, in *GetTokensRequest, opts ...grpc.CallOption) (M2MOAuthService_GetTokensClient, error) + // Blacklists/revokes tokens + BlacklistTokens(ctx context.Context, in *BlacklistTokensRequest, opts ...grpc.CallOption) (*BlacklistTokensResponse, error) +} + +type m2MOAuthServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewM2MOAuthServiceClient(cc grpc.ClientConnInterface) M2MOAuthServiceClient { + return &m2MOAuthServiceClient{cc} +} + +func (c *m2MOAuthServiceClient) CreateToken(ctx context.Context, in *CreateTokenRequest, opts ...grpc.CallOption) (*CreateTokenResponse, error) { + out := new(CreateTokenResponse) + err := c.cc.Invoke(ctx, M2MOAuthService_CreateToken_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *m2MOAuthServiceClient) GetTokens(ctx context.Context, in *GetTokensRequest, opts ...grpc.CallOption) (M2MOAuthService_GetTokensClient, error) { + stream, err := c.cc.NewStream(ctx, &M2MOAuthService_ServiceDesc.Streams[0], M2MOAuthService_GetTokens_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &m2MOAuthServiceGetTokensClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type M2MOAuthService_GetTokensClient interface { + Recv() (*Token, error) + grpc.ClientStream +} + +type m2MOAuthServiceGetTokensClient struct { + grpc.ClientStream +} + +func (x *m2MOAuthServiceGetTokensClient) Recv() (*Token, error) { + m := new(Token) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *m2MOAuthServiceClient) BlacklistTokens(ctx context.Context, in *BlacklistTokensRequest, opts ...grpc.CallOption) (*BlacklistTokensResponse, error) { + out := new(BlacklistTokensResponse) + err := c.cc.Invoke(ctx, M2MOAuthService_BlacklistTokens_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// M2MOAuthServiceServer is the server API for M2MOAuthService service. +// All implementations must embed UnimplementedM2MOAuthServiceServer +// for forward compatibility +type M2MOAuthServiceServer interface { + // Creates a new token + CreateToken(context.Context, *CreateTokenRequest) (*CreateTokenResponse, error) + // Returns all tokens of the owner + GetTokens(*GetTokensRequest, M2MOAuthService_GetTokensServer) error + // Blacklists/revokes tokens + BlacklistTokens(context.Context, *BlacklistTokensRequest) (*BlacklistTokensResponse, error) + mustEmbedUnimplementedM2MOAuthServiceServer() +} + +// UnimplementedM2MOAuthServiceServer must be embedded to have forward compatible implementations. +type UnimplementedM2MOAuthServiceServer struct { +} + +func (UnimplementedM2MOAuthServiceServer) CreateToken(context.Context, *CreateTokenRequest) (*CreateTokenResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateToken not implemented") +} +func (UnimplementedM2MOAuthServiceServer) GetTokens(*GetTokensRequest, M2MOAuthService_GetTokensServer) error { + return status.Errorf(codes.Unimplemented, "method GetTokens not implemented") +} +func (UnimplementedM2MOAuthServiceServer) BlacklistTokens(context.Context, *BlacklistTokensRequest) (*BlacklistTokensResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method BlacklistTokens not implemented") +} +func (UnimplementedM2MOAuthServiceServer) mustEmbedUnimplementedM2MOAuthServiceServer() {} + +// UnsafeM2MOAuthServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to M2MOAuthServiceServer will +// result in compilation errors. +type UnsafeM2MOAuthServiceServer interface { + mustEmbedUnimplementedM2MOAuthServiceServer() +} + +func RegisterM2MOAuthServiceServer(s grpc.ServiceRegistrar, srv M2MOAuthServiceServer) { + s.RegisterService(&M2MOAuthService_ServiceDesc, srv) +} + +func _M2MOAuthService_CreateToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(M2MOAuthServiceServer).CreateToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: M2MOAuthService_CreateToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(M2MOAuthServiceServer).CreateToken(ctx, req.(*CreateTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _M2MOAuthService_GetTokens_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetTokensRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(M2MOAuthServiceServer).GetTokens(m, &m2MOAuthServiceGetTokensServer{stream}) +} + +type M2MOAuthService_GetTokensServer interface { + Send(*Token) error + grpc.ServerStream +} + +type m2MOAuthServiceGetTokensServer struct { + grpc.ServerStream +} + +func (x *m2MOAuthServiceGetTokensServer) Send(m *Token) error { + return x.ServerStream.SendMsg(m) +} + +func _M2MOAuthService_BlacklistTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BlacklistTokensRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(M2MOAuthServiceServer).BlacklistTokens(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: M2MOAuthService_BlacklistTokens_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(M2MOAuthServiceServer).BlacklistTokens(ctx, req.(*BlacklistTokensRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// M2MOAuthService_ServiceDesc is the grpc.ServiceDesc for M2MOAuthService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var M2MOAuthService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "m2moauthserver.pb.M2MOAuthService", + HandlerType: (*M2MOAuthServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateToken", + Handler: _M2MOAuthService_CreateToken_Handler, + }, + { + MethodName: "BlacklistTokens", + Handler: _M2MOAuthService_BlacklistTokens_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "GetTokens", + Handler: _M2MOAuthService_GetTokens_Handler, + ServerStreams: true, + }, + }, + Metadata: "m2m-oauth-server/pb/service.proto", +} diff --git a/m2m-oauth-server/pb/tags.go b/m2m-oauth-server/pb/tags.go new file mode 100644 index 000000000..f77f1d074 --- /dev/null +++ b/m2m-oauth-server/pb/tags.go @@ -0,0 +1,12 @@ +package pb + +const ( + ExpirationKey = "expiration" + OwnerKey = "owner" + BlackListedFlagKey = BlackListedKey + ".flag" + BlackListedTimestampKey = BlackListedKey + ".timestamp" + BlackListedKey = "blacklisted" + TimestampKey = "timestamp" + AudienceKey = "audience" + IssuedAtKey = "issuedAt" +) diff --git a/m2m-oauth-server/pb/token.go b/m2m-oauth-server/pb/token.go new file mode 100644 index 000000000..82174ce8c --- /dev/null +++ b/m2m-oauth-server/pb/token.go @@ -0,0 +1,85 @@ +package pb + +import ( + "errors" + "fmt" + + pkgMongo "github.com/plgd-dev/hub/v2/pkg/mongodb" +) + +var errTokenIsNil = errors.New("Token is nil") + +func (x *Token) Validate() error { + if x == nil { + return errTokenIsNil + } + if x.GetId() == "" { + return errors.New("Token.Id is empty") + } + if x.GetOwner() == "" { + return errors.New("Token.Owner is empty") + } + if x.GetClientId() == "" { + return errors.New("Token.ClientId is empty") + } + if x.GetIssuedAt() == 0 { + return errors.New("Token.Timestamp is empty") + } + return nil +} + +func (x *Token) jsonToBSONTag(json map[string]interface{}) error { + json["_id"] = x.GetId() + delete(json, "id") + if _, err := pkgMongo.ConvertStringValueToInt64(json, false, "."+IssuedAtKey); err != nil { + return fmt.Errorf("cannot convert issueAt to int64: %w", err) + } + if _, err := pkgMongo.ConvertStringValueToInt64(json, true, "."+ExpirationKey); err != nil { + return fmt.Errorf("cannot convert expiration to int64: %w", err) + } + if _, err := pkgMongo.ConvertStringValueToInt64(json, true, "."+BlackListedKey+"."+TimestampKey); err != nil { + return fmt.Errorf("cannot convert blacklisted.timestamp to int64: %w", err) + } + return nil +} + +func (x *Token) MarshalBSON() ([]byte, error) { + if x == nil { + return nil, errTokenIsNil + } + return pkgMongo.MarshalProtoBSON(x, x.jsonToBSONTag) +} + +func (x *Token) UnmarshalBSON(data []byte) error { + if x == nil { + return errTokenIsNil + } + var id string + update := func(json map[string]interface{}) error { + idI, ok := json["_id"] + if ok { + id = idI.(string) + } + delete(json, "_id") + return nil + } + err := pkgMongo.UnmarshalProtoBSON(data, x, update) + if err != nil { + return err + } + if x.GetId() == "" && id != "" { + x.Id = id + } + return nil +} + +func (x *Token_BlackListed) jsonToBSONTag(json map[string]interface{}) error { + if _, err := pkgMongo.ConvertStringValueToInt64(json, false, "."+TimestampKey); err != nil { + return fmt.Errorf("cannot convert timestamp to int64: %w", err) + } + return nil +} + +func (x *Token_BlackListed) MarshalBSON() ([]byte, error) { + return pkgMongo.MarshalProtoBSON(x, x.jsonToBSONTag) +} diff --git a/m2m-oauth-server/service/config.go b/m2m-oauth-server/service/config.go index b2ec547cb..28a6e16cc 100644 --- a/m2m-oauth-server/service/config.go +++ b/m2m-oauth-server/service/config.go @@ -2,103 +2,19 @@ package service import ( "fmt" - "time" + "net" + oauthsigner "github.com/plgd-dev/hub/v2/m2m-oauth-server/oauthSigner" + grpcService "github.com/plgd-dev/hub/v2/m2m-oauth-server/service/grpc" + storeConfig "github.com/plgd-dev/hub/v2/m2m-oauth-server/store/config" "github.com/plgd-dev/hub/v2/pkg/config" - "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" "github.com/plgd-dev/hub/v2/pkg/log" "github.com/plgd-dev/hub/v2/pkg/net/http" "github.com/plgd-dev/hub/v2/pkg/net/http/server" - "github.com/plgd-dev/hub/v2/pkg/net/listener" - "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" ) -type AsymmetricKey struct { - PrivateFile string - PublicFile string -} - -type AccessTokenType string - -const AccessTokenType_JWT AccessTokenType = "jwt" - -type GrantType string - -const ( - GrantTypeClientCredentials GrantType = "client_credentials" -) - -type PrivateKeyJWTConfig struct { - Enabled bool `yaml:"enabled"` - Authorization validator.Config `yaml:"authorization,omitempty"` -} - -func (c *PrivateKeyJWTConfig) Validate() error { - if !c.Enabled { - return nil - } - if err := c.Authorization.Validate(); err != nil { - return fmt.Errorf("authorization.%w", err) - } - return nil -} - -type Client struct { - ID string `yaml:"id"` - SecretFile urischeme.URIScheme `yaml:"secretFile"` - AccessTokenLifetime time.Duration `yaml:"accessTokenLifetime"` - AllowedGrantTypes []GrantType `yaml:"allowedGrantTypes"` - AllowedAudiences []string `yaml:"allowedAudiences"` - AllowedScopes []string `yaml:"allowedScopes"` - JWTPrivateKey PrivateKeyJWTConfig `yaml:"jwtPrivateKey"` - InsertTokenClaims map[string]interface{} `yaml:"insertTokenClaims"` - - // runtime - secret string `yaml:"-"` -} - -func (c *Client) Validate() error { - if c.ID == "" { - return fmt.Errorf("id('%v')", c.ID) - } - if !c.JWTPrivateKey.Enabled { - if c.SecretFile == "" { - return fmt.Errorf("secretFile('%v') - one of [secretFile, privateKeyJWT] need to be set", c.SecretFile) - } - data, err := c.SecretFile.Read() - if err != nil { - return fmt.Errorf("secretFile('%v') - %w", c.SecretFile, err) - } - c.secret = string(data) - } - if len(c.AllowedGrantTypes) == 0 { - return fmt.Errorf("allowedGrantTypes('%v') - is empty", c.AllowedGrantTypes) - } - for _, gt := range c.AllowedGrantTypes { - switch gt { - case GrantTypeClientCredentials: - default: - return fmt.Errorf("allowedGrantTypes('%v') - only %v is supported", c.AllowedGrantTypes, GrantTypeClientCredentials) - } - } - if err := c.JWTPrivateKey.Validate(); err != nil { - return fmt.Errorf("privateKeyJWT.%w", err) - } - return nil -} - -type OAuthClientsConfig []*Client - -func (c OAuthClientsConfig) Find(id string) *Client { - for _, client := range c { - if client.ID == id { - return client - } - } - return nil -} - type ClientsConfig struct { + Storage storeConfig.Config `yaml:"storage" json:"storage"` OpenTelemetryCollector http.OpenTelemetryCollectorConfig `yaml:"openTelemetryCollector" json:"openTelemetryCollector"` } @@ -106,15 +22,22 @@ func (c *ClientsConfig) Validate() error { if err := c.OpenTelemetryCollector.Validate(); err != nil { return fmt.Errorf("openTelemetryCollector.%w", err) } + if err := c.Storage.Validate(); err != nil { + return fmt.Errorf("storage.%w", err) + } return nil } // Config represents application configuration type Config struct { - Log log.Config `yaml:"log" json:"log"` - APIs APIsConfig `yaml:"apis" json:"apis"` - Clients ClientsConfig `yaml:"clients" json:"clients"` - OAuthSigner OAuthSignerConfig `yaml:"oauthSigner" json:"oauthSigner"` + Log log.Config `yaml:"log" json:"log"` + APIs APIsConfig `yaml:"apis" json:"apis"` + Clients ClientsConfig `yaml:"clients" json:"clients"` + OAuthSigner oauthsigner.Config `yaml:"oauthSigner" json:"oauthSigner"` +} + +func (c *Config) String() string { + return config.ToString(c) } func (c *Config) Validate() error { @@ -135,51 +58,28 @@ func (c *Config) Validate() error { // Config represent application configuration type APIsConfig struct { - HTTP HTTPConfig `yaml:"http" json:"http"` + HTTP HTTPConfig `yaml:"http" json:"http"` + GRPC grpcService.Config `yaml:"grpc" json:"grpc"` } func (c *APIsConfig) Validate() error { if err := c.HTTP.Validate(); err != nil { return fmt.Errorf("http.%w", err) } + if err := c.GRPC.Validate(); err != nil { + return fmt.Errorf("grpc.%w", err) + } return nil } type HTTPConfig struct { - Connection listener.Config `yaml:",inline" json:",inline"` - Server server.Config `yaml:",inline" json:",inline"` + Addr string `yaml:"address" json:"address"` + Server server.Config `yaml:",inline" json:",inline"` } func (c *HTTPConfig) Validate() error { - return c.Connection.Validate() -} - -type OAuthSignerConfig struct { - PrivateKeyFile urischeme.URIScheme `yaml:"privateKeyFile" json:"privateKeyFile"` - Domain string `yaml:"domain" json:"domain"` - OwnerClaim string `yaml:"ownerClaim" json:"ownerClaim"` - DeviceIDClaim string `yaml:"deviceIDClaim" json:"deviceIDClaim"` - Clients OAuthClientsConfig `yaml:"clients" json:"clients"` -} - -func (c *OAuthSignerConfig) Validate() error { - if c.PrivateKeyFile == "" { - return fmt.Errorf("privateKeyFile('%v')", c.PrivateKeyFile) - } - if c.Domain == "" { - return fmt.Errorf("domain('%v')", c.Domain) - } - if len(c.Clients) == 0 { - return fmt.Errorf("clients('%v')", c.Clients) - } - for idx, client := range c.Clients { - if err := client.Validate(); err != nil { - return fmt.Errorf("clients[%v].%w", idx, err) - } + if _, err := net.ResolveTCPAddr("tcp", c.Addr); err != nil { + return fmt.Errorf("address('%v') - %w", c.Addr, err) } return nil } - -func (c Config) String() string { - return config.ToString(c) -} diff --git a/m2m-oauth-server/service/expiredUpdatesChecker.go b/m2m-oauth-server/service/expiredUpdatesChecker.go new file mode 100644 index 000000000..779587676 --- /dev/null +++ b/m2m-oauth-server/service/expiredUpdatesChecker.go @@ -0,0 +1,21 @@ +package service + +import ( + "fmt" + "time" + + "github.com/go-co-op/gocron/v2" +) + +func NewExpiredUpdatesChecker(cleanUpExpiredUpdates string, withSeconds bool, onCheckTimeout func()) (gocron.Scheduler, error) { + s, err := gocron.NewScheduler(gocron.WithLocation(time.Local)) //nolint:gosmopolitan + if err != nil { + return nil, fmt.Errorf("cannot create cron job: %w", err) + } + _, err = s.NewJob(gocron.CronJob(cleanUpExpiredUpdates, withSeconds), gocron.NewTask(onCheckTimeout)) + if err != nil { + return nil, fmt.Errorf("cannot create cron job: %w", err) + } + s.Start() + return s, nil +} diff --git a/m2m-oauth-server/service/grpc/config.go b/m2m-oauth-server/service/grpc/config.go new file mode 100644 index 000000000..655e8170e --- /dev/null +++ b/m2m-oauth-server/service/grpc/config.go @@ -0,0 +1,7 @@ +package grpc + +import ( + "github.com/plgd-dev/hub/v2/pkg/net/grpc/server" +) + +type Config = server.Config diff --git a/m2m-oauth-server/service/grpc/server.go b/m2m-oauth-server/service/grpc/server.go new file mode 100644 index 000000000..9133bcf08 --- /dev/null +++ b/m2m-oauth-server/service/grpc/server.go @@ -0,0 +1,166 @@ +package grpc + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/lestrrat-go/jwx/v2/jwk" + oauthsigner "github.com/plgd-dev/hub/v2/m2m-oauth-server/oauthSigner" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/store" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgTime "github.com/plgd-dev/hub/v2/pkg/time" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +// M2MOAuthServiceServer handles incoming requests. +type M2MOAuthServiceServer struct { + pb.UnimplementedM2MOAuthServiceServer + + signer *oauthsigner.OAuthSigner + store store.Store + logger log.Logger +} + +func NewM2MOAuthServerServer(store store.Store, signer *oauthsigner.OAuthSigner, logger log.Logger) *M2MOAuthServiceServer { + return &M2MOAuthServiceServer{ + store: store, + logger: logger, + signer: signer, + } +} + +func (s *M2MOAuthServiceServer) getOwner(ctx context.Context) (string, error) { + ownerFromToken, err := pkgGrpc.OwnerFromTokenMD(ctx, s.signer.GetOwnerClaim()) + if err != nil { + return "", err + } + return ownerFromToken, nil +} + +func getGRPCErrorCode(err error) codes.Code { + if errors.Is(err, store.ErrInvalidArgument) { + return codes.InvalidArgument + } + return codes.Internal +} + +func errCannotCreateConfiguration(err error) error { + return fmt.Errorf("cannot get configuration: %w", err) +} + +func errCannotCreateToken(err error) error { + return fmt.Errorf("cannot create token: %w", err) +} + +func (s *M2MOAuthServiceServer) CreateToken(ctx context.Context, req *pb.CreateTokenRequest) (*pb.CreateTokenResponse, error) { + tokenReq := tokenRequest{ + host: s.signer.GetDomain(), + tokenType: oauthsigner.AccessTokenType_JWT, + issuedAt: time.Now(), + CreateTokenRequest: req, + } + clientCfg := s.signer.GetClients().Find(tokenReq.CreateTokenRequest.GetClientId()) + if clientCfg == nil { + return nil, status.Errorf(codes.Unauthenticated, "%v", errCannotCreateToken(fmt.Errorf("client(%v) not found", tokenReq.CreateTokenRequest.GetClientId()))) + } + tokenReq.owner = clientCfg.Owner + if err := s.validateTokenRequest(ctx, clientCfg, &tokenReq); err != nil { + return nil, status.Errorf(codes.Unauthenticated, "%v", errCannotCreateToken(fmt.Errorf("invalid request: %w", err))) + } + var originalTokenClaims *structpb.Value + if len(tokenReq.originalTokenClaims) > 0 { + var err error + originalTokenClaims, err = structpb.NewValue(map[string]interface{}(tokenReq.originalTokenClaims)) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "%v", errCannotCreateToken(fmt.Errorf("cannot convert original token claims: %w", err))) + } + } + tokenReq.scopes = strings.Join(req.GetScope(), " ") + tokenReq.deviceIDClaim = s.signer.GetDeviceIDClaim() + tokenReq.ownerClaim = s.signer.GetOwnerClaim() + tokenReq.id = uuid.NewString() + tokenReq.expiration = getExpirationTime(clientCfg, tokenReq) + tokenReq.subject = getSubject(clientCfg, tokenReq) + accessToken, err := s.generateAccessToken( + clientCfg, + tokenReq) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "%v", errCannotCreateToken(err)) + } + + token, err := s.store.CreateToken(ctx, tokenReq.owner, &pb.Token{ + Id: tokenReq.id, + Name: tokenReq.CreateTokenRequest.GetTokenName(), + Owner: tokenReq.owner, + IssuedAt: tokenReq.issuedAt.Unix(), + Audience: tokenReq.CreateTokenRequest.GetAudience(), + Scope: tokenReq.CreateTokenRequest.GetScope(), + Expiration: pkgTime.UnixSec(tokenReq.expiration), + ClientId: tokenReq.CreateTokenRequest.GetClientId(), + OriginalTokenClaims: originalTokenClaims, + Subject: tokenReq.subject, + }) + if err != nil { + return nil, status.Errorf(getGRPCErrorCode(err), "%v", errCannotCreateConfiguration(err)) + } + var expiresIn int64 + if !tokenReq.expiration.IsZero() { + expiresIn = int64(time.Until(tokenReq.expiration).Seconds()) + } + return &pb.CreateTokenResponse{ + AccessToken: accessToken, + TokenType: "Bearer", + ExpiresIn: expiresIn, + Scope: token.GetScope(), + }, nil +} + +func errCannotGetTokens(err error) error { + return fmt.Errorf("cannot get tokens: %w", err) +} + +func (s *M2MOAuthServiceServer) GetTokens(req *pb.GetTokensRequest, srv pb.M2MOAuthService_GetTokensServer) error { + owner, err := s.getOwner(srv.Context()) + if err != nil { + return err + } + err = s.store.GetTokens(srv.Context(), owner, req, func(v *pb.Token) error { + return srv.Send(v) + }) + if err != nil { + return status.Errorf(getGRPCErrorCode(err), "%v", errCannotGetTokens(err)) + } + return nil +} + +func errCannotBlacklistTokens(err error) error { + return fmt.Errorf("cannot blacklist tokens: %w", err) +} + +func (s *M2MOAuthServiceServer) BlacklistTokens(ctx context.Context, req *pb.BlacklistTokensRequest) (*pb.BlacklistTokensResponse, error) { + owner, err := s.getOwner(ctx) + if err != nil { + return nil, err + } + resp, err := s.store.BlacklistTokens(ctx, owner, req) + if err != nil { + return nil, status.Errorf(getGRPCErrorCode(err), "%v", errCannotBlacklistTokens(err)) + } + return resp, nil +} + +func (s *M2MOAuthServiceServer) GetJWK() jwk.Key { + return s.signer.GetJWK() +} + +func (s *M2MOAuthServiceServer) GetDomain() string { + return s.signer.GetDomain() +} diff --git a/m2m-oauth-server/service/grpc/service.go b/m2m-oauth-server/service/grpc/service.go new file mode 100644 index 000000000..dbe0569f1 --- /dev/null +++ b/m2m-oauth-server/service/grpc/service.go @@ -0,0 +1,35 @@ +package grpc + +import ( + "fmt" + + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/net/grpc/server" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" + "go.opentelemetry.io/otel/trace" +) + +type Service struct { + *server.Server +} + +func New(config Config, m2mOAuthServiceServer *M2MOAuthServiceServer, validator *validator.Validator, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*Service, error) { + opts, err := server.MakeDefaultOptions(server.NewAuth(validator, server.WithWhiteListedMethods(pb.M2MOAuthService_CreateToken_FullMethodName)), logger, tracerProvider) + if err != nil { + return nil, fmt.Errorf("cannot create grpc server options: %w", err) + } + server, err := server.New(config, fileWatcher, logger, opts...) + if err != nil { + return nil, err + } + pb.RegisterM2MOAuthServiceServer(server.Server, m2mOAuthServiceServer) + + // M2MOAuthService needs to stop gracefully to ensure that all commands are processed. + server.SetGracefulStop(true) + + return &Service{ + Server: server, + }, nil +} diff --git a/m2m-oauth-server/service/grpc/token.go b/m2m-oauth-server/service/grpc/token.go new file mode 100644 index 000000000..3730ebd62 --- /dev/null +++ b/m2m-oauth-server/service/grpc/token.go @@ -0,0 +1,291 @@ +package grpc + +import ( + "context" + "errors" + "fmt" + "time" + + goJwt "github.com/golang-jwt/jwt/v5" + "github.com/lestrrat-go/jwx/v2/jwt" + oauthsigner "github.com/plgd-dev/hub/v2/m2m-oauth-server/oauthSigner" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" + pkgJwt "github.com/plgd-dev/hub/v2/pkg/security/jwt" +) + +func setKeyError(key string, err error) error { + return fmt.Errorf("failed to set %v: %w", key, err) +} + +func setKeyErrorExt(key, info interface{}, err error) error { + return fmt.Errorf("failed to set %v('%v'): %w", key, info, err) +} + +func makeAccessToken(clientCfg *oauthsigner.Client, tokenReq tokenRequest) (jwt.Token, error) { + token := jwt.New() + + claims := map[string]interface{}{ + jwt.JwtIDKey: tokenReq.id, + jwt.SubjectKey: tokenReq.subject, + jwt.AudienceKey: tokenReq.host, + jwt.IssuedAtKey: tokenReq.issuedAt, + uri.ScopeKey: tokenReq.scopes, + uri.ClientIDKey: clientCfg.ID, + jwt.IssuerKey: tokenReq.host, + } + for key, val := range claims { + if err := token.Set(key, val); err != nil { + return nil, setKeyError(key, err) + } + } + if !tokenReq.expiration.IsZero() { + if err := token.Set(jwt.ExpirationKey, tokenReq.expiration); err != nil { + return nil, setKeyError(jwt.ExpirationKey, err) + } + } + if err := setDeviceIDClaim(token, tokenReq); err != nil { + return nil, err + } + if err := setName(token, tokenReq); err != nil { + return nil, err + } + if err := setOwnerClaim(token, tokenReq); err != nil { + return nil, err + } + if err := setOriginTokenClaims(token, tokenReq); err != nil { + return nil, err + } + + for k, v := range clientCfg.InsertTokenClaims { + if _, ok := token.Get(k); ok { + continue + } + if err := token.Set(k, v); err != nil { + return nil, setKeyErrorExt(k, v, err) + } + } + + return token, nil +} + +func getSubject(clientCfg *oauthsigner.Client, tokenReq tokenRequest) string { + if tokenReq.subject != "" { + return tokenReq.subject + } + if tokenReq.owner != "" { + return tokenReq.owner + } + return clientCfg.ID +} + +func setDeviceIDClaim(token jwt.Token, tokenReq tokenRequest) error { + if tokenReq.deviceID != "" && tokenReq.deviceIDClaim != "" { + return token.Set(tokenReq.deviceIDClaim, tokenReq.deviceID) + } + return nil +} + +func setOwnerClaim(token jwt.Token, tokenReq tokenRequest) error { + if tokenReq.owner != "" && tokenReq.ownerClaim != "" { + return token.Set(tokenReq.ownerClaim, tokenReq.owner) + } + return nil +} + +func setName(token jwt.Token, tokenReq tokenRequest) error { + if tokenReq.CreateTokenRequest.GetTokenName() != "" && tokenReq.ownerClaim != "name" { + return token.Set("name", tokenReq.CreateTokenRequest.GetTokenName()) + } + return nil +} + +func setOriginTokenClaims(token jwt.Token, tokenReq tokenRequest) error { + if len(tokenReq.originalTokenClaims) > 0 { + return token.Set(uri.OriginalTokenClaims, tokenReq.originalTokenClaims) + } + return nil +} + +func getExpirationTime(clientCfg *oauthsigner.Client, tokenReq tokenRequest) time.Time { + var wantExpiration time.Time + if tokenReq.CreateTokenRequest.GetExpiration() > 0 { + wantExpiration = time.Unix(tokenReq.CreateTokenRequest.GetExpiration(), 0) + } + if !wantExpiration.IsZero() && tokenReq.issuedAt.After(wantExpiration) { + return time.Time{} + } + if clientCfg.AccessTokenLifetime == 0 { + if !wantExpiration.IsZero() { + return wantExpiration + } + return time.Time{} + } + if wantExpiration.IsZero() { + return tokenReq.issuedAt.Add(clientCfg.AccessTokenLifetime) + } + clientExpiration := tokenReq.issuedAt.Add(clientCfg.AccessTokenLifetime) + if clientExpiration.Before(wantExpiration) { + return clientExpiration + } + return wantExpiration +} + +func (s *M2MOAuthServiceServer) generateAccessToken(clientCfg *oauthsigner.Client, tokenReq tokenRequest) (string, error) { + token, err := makeAccessToken(clientCfg, tokenReq) + if err != nil { + return "", fmt.Errorf("failed to make token: %w", err) + } + payload, err := s.signer.Sign(token) + if err != nil { + return "", fmt.Errorf("failed to sign token: %w", err) + } + return string(payload), nil +} + +type tokenRequest struct { + *pb.CreateTokenRequest + + id string `json:"-"` + deviceID string `json:"-"` + owner string `json:"-"` + subject string `json:"-"` + host string `json:"-"` + scopes string `json:"-"` + ownerClaim string `json:"-"` + deviceIDClaim string `json:"-"` + tokenType oauthsigner.AccessTokenType `json:"-"` + originalTokenClaims goJwt.MapClaims `json:"-"` + issuedAt time.Time `json:"-"` + expiration time.Time `json:"-"` +} + +func sliceContains[T comparable](s []T, sub []T) bool { + // sub must be non-empty + if len(s) > 0 && len(sub) == 0 { + return false + } + check := make(map[T]struct{}, len(sub)) + for _, e := range sub { + check[e] = struct{}{} + } + for _, e := range s { + delete(check, e) + } + return len(check) == 0 +} + +func validateExpiration(clientCfg *oauthsigner.Client, tokenReq *tokenRequest) error { + if tokenReq.CreateTokenRequest.GetExpiration() > 0 { + if tokenReq.CreateTokenRequest.GetExpiration() < tokenReq.issuedAt.Unix() { + return fmt.Errorf("expiration(%v) must be greater than issuedAt(%v)", time.Unix(tokenReq.CreateTokenRequest.GetExpiration(), 0), tokenReq.issuedAt) + } + if clientCfg.AccessTokenLifetime > 0 { + if tokenReq.CreateTokenRequest.GetExpiration() > tokenReq.issuedAt.Add(clientCfg.AccessTokenLifetime).Unix() { + return fmt.Errorf("expiration(%v) must be less than or equal to issuedAt + client accessTokenLifetime(%v)", time.Unix(tokenReq.CreateTokenRequest.GetExpiration(), 0), tokenReq.issuedAt.Add(clientCfg.AccessTokenLifetime)) + } + } + } + return nil +} + +func (s *M2MOAuthServiceServer) validateTokenRequest(ctx context.Context, clientCfg *oauthsigner.Client, tokenReq *tokenRequest) error { + if err := validateGrantType(clientCfg, tokenReq); err != nil { + return err + } + if err := validateClient(clientCfg, tokenReq); err != nil { + return err + } + if err := validateExpiration(clientCfg, tokenReq); err != nil { + return err + } + if err := validateClientAssertionType(clientCfg, tokenReq); err != nil { + return err + } + if err := s.validateClientAssertion(ctx, tokenReq); err != nil { + return err + } + if err := validateAudience(clientCfg, tokenReq); err != nil { + return err + } + if err := validateScopes(clientCfg, tokenReq); err != nil { + return err + } + return nil +} + +func validateClient(clientCfg *oauthsigner.Client, tokenReq *tokenRequest) error { + if clientCfg == nil { + return fmt.Errorf("client(%v) not found", tokenReq.CreateTokenRequest.GetClientId()) + } + if clientCfg.Secret != "" && !clientCfg.JWTPrivateKey.Enabled && clientCfg.Secret != tokenReq.CreateTokenRequest.GetClientSecret() { + return errors.New("invalid client secret") + } + return nil +} + +func validateGrantType(clientCfg *oauthsigner.Client, tokenReq *tokenRequest) error { + // clientCfg.AllowedGrantTypes is always non-empty + if !sliceContains(clientCfg.AllowedGrantTypes, []oauthsigner.GrantType{oauthsigner.GrantType(tokenReq.CreateTokenRequest.GetGrantType())}) { + return fmt.Errorf("invalid grant type(%v)", tokenReq.CreateTokenRequest.GetGrantType()) + } + return nil +} + +func validateAudience(clientCfg *oauthsigner.Client, tokenReq *tokenRequest) error { + if !sliceContains(clientCfg.AllowedAudiences, tokenReq.CreateTokenRequest.GetAudience()) { + return fmt.Errorf("invalid audience(%v)", tokenReq.CreateTokenRequest.GetAudience()) + } + return nil +} + +func validateScopes(clientCfg *oauthsigner.Client, tokenReq *tokenRequest) error { + if len(tokenReq.CreateTokenRequest.GetScope()) == 0 { + tokenReq.CreateTokenRequest.Scope = clientCfg.AllowedScopes + } + if !sliceContains(clientCfg.AllowedScopes, tokenReq.CreateTokenRequest.GetScope()) { + return fmt.Errorf("invalid scope(%v)", tokenReq.CreateTokenRequest.GetScope()) + } + return nil +} + +func validateClientAssertionType(clientCfg *oauthsigner.Client, tokenReq *tokenRequest) error { + if tokenReq.CreateTokenRequest.GetClientAssertionType() != "" && clientCfg.JWTPrivateKey.Enabled && tokenReq.CreateTokenRequest.GetClientAssertionType() != uri.ClientAssertionTypeJWT { + return fmt.Errorf("invalid client assertion type(%v)", tokenReq.CreateTokenRequest.GetClientAssertionType()) + } + return nil +} + +func (s *M2MOAuthServiceServer) validateClientAssertion(ctx context.Context, tokenReq *tokenRequest) error { + if tokenReq.CreateTokenRequest.GetClientAssertion() == "" { + return nil + } + v, ok := s.signer.GetValidator(tokenReq.CreateTokenRequest.GetClientId()) + if !ok { + return errors.New("invalid client assertion") + } + token, err := v.GetParser().ParseWithContext(ctx, tokenReq.CreateTokenRequest.GetClientAssertion()) + if err != nil { + return fmt.Errorf("invalid client assertion: %w", err) + } + tokenReq.originalTokenClaims = token + claims := pkgJwt.Claims(token) + owner, err := claims.GetOwner(s.signer.GetOwnerClaim()) + if err != nil { + return fmt.Errorf("invalid client assertion - claim owner: %w", err) + } + tokenReq.owner = owner + sub, err := claims.GetSubject() + if err != nil { + return fmt.Errorf("invalid client assertion - claim sub: %w", err) + } + tokenReq.subject = sub + if s.signer.GetDeviceIDClaim() == "" { + return nil + } + deviceID, err := claims.GetDeviceID(s.signer.GetDeviceIDClaim()) + if err == nil { + tokenReq.deviceID = deviceID + } + return nil +} diff --git a/m2m-oauth-server/service/grpc/token_internal_test.go b/m2m-oauth-server/service/grpc/token_internal_test.go new file mode 100644 index 000000000..4ded85f52 --- /dev/null +++ b/m2m-oauth-server/service/grpc/token_internal_test.go @@ -0,0 +1,112 @@ +package grpc + +import ( + "testing" + "time" + + oauthsigner "github.com/plgd-dev/hub/v2/m2m-oauth-server/oauthSigner" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + "github.com/stretchr/testify/require" +) + +func TestGetExpirationTime(t *testing.T) { + now := time.Now() + type args struct { + clientCfg *oauthsigner.Client + tokenReq tokenRequest + } + tests := []struct { + name string + args args + want time.Time + }{ + { + name: "client access token lifetime = 0h, token expiration = now + 1h", + args: args{ + clientCfg: &oauthsigner.Client{ + AccessTokenLifetime: 0, + }, + tokenReq: tokenRequest{ + CreateTokenRequest: &pb.CreateTokenRequest{ + Expiration: now.Unix() + int64(time.Hour.Seconds()), + }, + issuedAt: now, + }, + }, + want: now.Add(time.Hour), + }, + { + name: "client access token lifetime = 1h, token expiration = 0", + args: args{ + clientCfg: &oauthsigner.Client{ + AccessTokenLifetime: time.Hour, + }, + tokenReq: tokenRequest{ + CreateTokenRequest: &pb.CreateTokenRequest{}, + issuedAt: now, + }, + }, + want: now.Add(time.Hour), + }, + { + name: "client access token lifetime = 1h, token expiration = now + 1h", + args: args{ + clientCfg: &oauthsigner.Client{ + AccessTokenLifetime: time.Hour, + }, + tokenReq: tokenRequest{ + CreateTokenRequest: &pb.CreateTokenRequest{ + Expiration: now.Unix() + int64(time.Hour.Seconds()), + }, + issuedAt: now, + }, + }, + want: now.Add(time.Hour), + }, + { + name: "client access token lifetime = 1h, token expiration = now + 2h", + args: args{ + clientCfg: &oauthsigner.Client{ + AccessTokenLifetime: time.Hour, + }, + tokenReq: tokenRequest{ + CreateTokenRequest: &pb.CreateTokenRequest{ + Expiration: now.Unix() + int64(time.Hour.Seconds()*2), + }, + issuedAt: now, + }, + }, + want: now.Add(time.Hour), + }, + { + name: "client access token lifetime = 0h, token expiration = now - 2h", + args: args{ + clientCfg: &oauthsigner.Client{}, + tokenReq: tokenRequest{ + CreateTokenRequest: &pb.CreateTokenRequest{ + Expiration: now.Unix() - int64(time.Hour.Seconds()*2), + }, + issuedAt: now, + }, + }, + want: time.Time{}, + }, + { + name: "client access token lifetime = 0h, token expiration = 0", + args: args{ + clientCfg: &oauthsigner.Client{}, + tokenReq: tokenRequest{ + CreateTokenRequest: &pb.CreateTokenRequest{}, + issuedAt: now, + }, + }, + want: time.Time{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getExpirationTime(tt.args.clientCfg, tt.args.tokenReq) + require.Equal(t, tt.want.Unix(), got.Unix()) + }) + } +} diff --git a/m2m-oauth-server/service/http/config.go b/m2m-oauth-server/service/http/config.go new file mode 100644 index 000000000..5ae74d064 --- /dev/null +++ b/m2m-oauth-server/service/http/config.go @@ -0,0 +1,13 @@ +package http + +import ( + "github.com/plgd-dev/hub/v2/pkg/net/http/server" + "github.com/plgd-dev/hub/v2/pkg/net/listener" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" +) + +type Config struct { + Connection listener.Config `yaml:",inline" json:",inline"` + Authorization validator.Config `yaml:"authorization" json:"authorization"` + Server server.Config `yaml:",inline" json:",inline"` +} diff --git a/m2m-oauth-server/service/http/createToken_test.go b/m2m-oauth-server/service/http/createToken_test.go new file mode 100644 index 000000000..ced03a017 --- /dev/null +++ b/m2m-oauth-server/service/http/createToken_test.go @@ -0,0 +1,82 @@ +package http_test + +import ( + "bytes" + "context" + "net/http" + "testing" + + "github.com/plgd-dev/go-coap/v3/message" + grpcPb "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + oauthsigner "github.com/plgd-dev/hub/v2/m2m-oauth-server/oauthSigner" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + m2mOauthServerTest "github.com/plgd-dev/hub/v2/m2m-oauth-server/test" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" + "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + testHttp "github.com/plgd-dev/hub/v2/test/http" + testService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" +) + +func TestCreateToken(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + tearDown := testService.SetUp(ctx, t) + defer tearDown() + + type args struct { + req *pb.CreateTokenRequest + } + tests := []struct { + name string + args args + wantHTTPCode int + want *pb.CreateTokenResponse + wantErr bool + }{ + { + name: "create token", + args: args{ + req: &pb.CreateTokenRequest{ + ClientId: m2mOauthServerTest.ServiceOAuthClient.ID, + ClientSecret: m2mOauthServerTest.GetSecret(t, m2mOauthServerTest.ServiceOAuthClient.ID), + GrantType: string(oauthsigner.GrantTypeClientCredentials), + TokenName: "service token", + }, + }, + wantHTTPCode: http.StatusOK, + want: &pb.CreateTokenResponse{ + TokenType: "Bearer", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := testHttp.GetContentData(&grpcPb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, tt.args.req), + }, message.AppJSON.String()) + require.NoError(t, err) + rb := testHttp.NewRequest(http.MethodPost, m2mOauthServerTest.HTTPURI(uri.Tokens), bytes.NewReader(data)) + rb = rb.ContentType(message.AppOcfCbor.String()) + resp := testHttp.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + var got pb.CreateTokenResponse + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotEmpty(t, got.GetAccessToken()) + require.Equal(t, tt.want.GetTokenType(), got.GetTokenType()) + }) + } +} diff --git a/m2m-oauth-server/service/getJWKs.go b/m2m-oauth-server/service/http/getJWKs.go similarity index 85% rename from m2m-oauth-server/service/getJWKs.go rename to m2m-oauth-server/service/http/getJWKs.go index 420766868..461f6ac85 100644 --- a/m2m-oauth-server/service/getJWKs.go +++ b/m2m-oauth-server/service/http/getJWKs.go @@ -1,4 +1,4 @@ -package service +package http import ( "net/http" @@ -10,7 +10,7 @@ import ( func (requestHandler *RequestHandler) getJWKs(w http.ResponseWriter, _ *http.Request) { resp := map[string]interface{}{ "keys": []jwk.Key{ - requestHandler.accessTokenJwkKey, + requestHandler.m2mOAuthServiceServer.GetJWK(), }, } diff --git a/m2m-oauth-server/service/getJWKs_test.go b/m2m-oauth-server/service/http/getJWKs_test.go similarity index 62% rename from m2m-oauth-server/service/getJWKs_test.go rename to m2m-oauth-server/service/http/getJWKs_test.go index b5ef793ff..1ee23a3da 100644 --- a/m2m-oauth-server/service/getJWKs_test.go +++ b/m2m-oauth-server/service/http/getJWKs_test.go @@ -1,31 +1,29 @@ -package service_test +package http_test import ( "context" "net/http" "testing" - m2mOauthServerTest "github.com/plgd-dev/hub/v2/m2m-oauth-server/test" "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" "github.com/plgd-dev/hub/v2/test/config" testHttp "github.com/plgd-dev/hub/v2/test/http" - "github.com/plgd-dev/hub/v2/test/oauth-server/test" + testService "github.com/plgd-dev/hub/v2/test/service" "github.com/plgd-dev/kit/v2/codec/json" "github.com/stretchr/testify/require" ) func TestRequestHandlerGetJWKs(t *testing.T) { - oauthServerTeardown := test.SetUp(t) - defer oauthServerTeardown() - - webTearDown := m2mOauthServerTest.SetUp(t) + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + webTearDown := testService.SetUp(ctx, t) defer webTearDown() - getJWKs(t) + getJWKs(ctx, t) } -func getJWKs(t *testing.T) map[string]interface{} { - getReq := testHttp.NewRequest(http.MethodGet, testHttp.HTTPS_SCHEME+config.M2M_OAUTH_SERVER_HTTP_HOST+uri.JWKs, nil).Build(context.Background(), t) +func getJWKs(ctx context.Context, t *testing.T) map[string]interface{} { + getReq := testHttp.NewRequest(http.MethodGet, testHttp.HTTPS_SCHEME+config.M2M_OAUTH_SERVER_HTTP_HOST+uri.JWKs, nil).Build(ctx, t) res := testHttp.Do(t, getReq) defer func() { _ = res.Body.Close() diff --git a/m2m-oauth-server/service/getOpenIDConfiguration.go b/m2m-oauth-server/service/http/getOpenIDConfiguration.go similarity index 53% rename from m2m-oauth-server/service/getOpenIDConfiguration.go rename to m2m-oauth-server/service/http/getOpenIDConfiguration.go index 2fa8bc861..7d6c96478 100644 --- a/m2m-oauth-server/service/getOpenIDConfiguration.go +++ b/m2m-oauth-server/service/http/getOpenIDConfiguration.go @@ -1,4 +1,4 @@ -package service +package http import ( "net/http" @@ -8,12 +8,17 @@ import ( "github.com/plgd-dev/hub/v2/pkg/security/openid" ) -func (requestHandler *RequestHandler) getOpenIDConfiguration(w http.ResponseWriter, _ *http.Request) { - v := openid.Config{ - Issuer: requestHandler.getDomain(), - TokenURL: requestHandler.getDomain() + uri.Token, - JWKSURL: requestHandler.getDomain() + uri.JWKs, +func GetOpenIDConfiguration(domain string) openid.Config { + return openid.Config{ + Issuer: domain, + TokenURL: domain + uri.Token, + JWKSURL: domain + uri.JWKs, + PlgdTokensEndpoint: domain + uri.Tokens, } +} + +func (requestHandler *RequestHandler) getOpenIDConfiguration(w http.ResponseWriter, _ *http.Request) { + v := GetOpenIDConfiguration(requestHandler.m2mOAuthServiceServer.GetDomain()) if err := jsonResponseWriter(w, v); err != nil { log.Errorf("failed to write response: %v", err) diff --git a/m2m-oauth-server/service/http/getOpenIDConfiguration_test.go b/m2m-oauth-server/service/http/getOpenIDConfiguration_test.go new file mode 100644 index 000000000..d28e81584 --- /dev/null +++ b/m2m-oauth-server/service/http/getOpenIDConfiguration_test.go @@ -0,0 +1,39 @@ +package http_test + +import ( + "context" + "net/http" + "testing" + + "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" + "github.com/plgd-dev/hub/v2/test/config" + testHttp "github.com/plgd-dev/hub/v2/test/http" + testService "github.com/plgd-dev/hub/v2/test/service" + "github.com/plgd-dev/kit/v2/codec/json" + "github.com/stretchr/testify/require" +) + +func TestGetOpenIDConfiguration(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + webTearDown := testService.SetUp(ctx, t) + defer webTearDown() + + getOpenIDConfiguration(ctx, t) +} + +func getOpenIDConfiguration(ctx context.Context, t *testing.T) { + getReq := testHttp.NewRequest(http.MethodGet, testHttp.HTTPS_SCHEME+config.M2M_OAUTH_SERVER_HTTP_HOST+uri.OpenIDConfiguration, nil).Build(ctx, t) + res := testHttp.Do(t, getReq) + defer func() { + _ = res.Body.Close() + }() + + var body map[string]interface{} + err := json.ReadFrom(res.Body, &body) + require.NoError(t, err) + require.NotEmpty(t, body["issuer"]) + require.NotEmpty(t, body["token_endpoint"]) + require.NotEmpty(t, body["jwks_uri"]) + require.NotEmpty(t, body["plgd_tokens_endpoint"]) +} diff --git a/m2m-oauth-server/service/http/getTokens_test.go b/m2m-oauth-server/service/http/getTokens_test.go new file mode 100644 index 000000000..dd7654b8c --- /dev/null +++ b/m2m-oauth-server/service/http/getTokens_test.go @@ -0,0 +1,280 @@ +package http_test + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "testing" + + "github.com/plgd-dev/go-coap/v3/message" + grpcPb "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + oauthsigner "github.com/plgd-dev/hub/v2/m2m-oauth-server/oauthSigner" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + m2mOauthServerTest "github.com/plgd-dev/hub/v2/m2m-oauth-server/test" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" + "github.com/plgd-dev/hub/v2/pkg/security/jwt" + "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + testHttp "github.com/plgd-dev/hub/v2/test/http" + testOAuthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + testService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" +) + +func createTokens(ctx context.Context, t *testing.T, createTokens []*pb.CreateTokenRequest) []string { + accessTokensIDs := make([]string, 0, len(createTokens)) + for _, createToken := range createTokens { + data, err := testHttp.GetContentData(&grpcPb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, createToken), + }, message.AppJSON.String()) + require.NoError(t, err) + rb := testHttp.NewRequest(http.MethodPost, m2mOauthServerTest.HTTPURI(uri.Tokens), bytes.NewReader(data)) + rb = rb.ContentType(message.AppOcfCbor.String()) + resp := testHttp.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, http.StatusOK, resp.StatusCode) + var got pb.CreateTokenResponse + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) + require.NoError(t, err) + claims, err := jwt.ParseToken(got.GetAccessToken()) + require.NoError(t, err) + name, err := claims.GetName() + require.NoError(t, err) + require.Equal(t, createToken.GetTokenName(), name) + id, err := claims.GetID() + require.NoError(t, err) + accessTokensIDs = append(accessTokensIDs, id) + } + return accessTokensIDs +} + +func blacklistTokens(ctx context.Context, t *testing.T, tokenIDs []string, token string) { + data, err := testHttp.GetContentData(&grpcPb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, &pb.BlacklistTokensRequest{ + IdFilter: tokenIDs, + }), + }, message.AppJSON.String()) + require.NoError(t, err) + rb := testHttp.NewRequest(http.MethodPost, m2mOauthServerTest.HTTPURI(uri.BlacklistTokens), bytes.NewReader(data)).AuthToken(token) + rb = rb.ContentType(message.AppOcfCbor.String()) + resp := testHttp.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestGetTokens(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + tearDown := testService.SetUp(ctx, t) + defer tearDown() + + token := testOAuthTest.GetDefaultAccessToken(t) + tokens := []*pb.CreateTokenRequest{ + { + ClientId: m2mOauthServerTest.ServiceOAuthClient.ID, + ClientSecret: m2mOauthServerTest.GetSecret(t, m2mOauthServerTest.ServiceOAuthClient.ID), + GrantType: string(oauthsigner.GrantTypeClientCredentials), + TokenName: "service token", + }, + { + ClientId: m2mOauthServerTest.JWTPrivateKeyOAuthClient.ID, + GrantType: string(oauthsigner.GrantTypeClientCredentials), + ClientAssertionType: string(uri.ClientAssertionTypeJWT), + ClientAssertion: token, + }, + } + blacklistedTokens := []*pb.CreateTokenRequest{ + { + ClientId: m2mOauthServerTest.ServiceOAuthClient.ID, + ClientSecret: m2mOauthServerTest.GetSecret(t, m2mOauthServerTest.ServiceOAuthClient.ID), + GrantType: string(oauthsigner.GrantTypeClientCredentials), + TokenName: "service token blacklisted", + }, + } + tokenIDs := createTokens(ctx, t, tokens) + blacklistTokenIDs := createTokens(ctx, t, blacklistedTokens) + + blacklistTokens(ctx, t, blacklistTokenIDs, token) + claims, err := jwt.ParseToken(token) + require.NoError(t, err) + + type args struct { + req *pb.GetTokensRequest + token string + } + tests := []struct { + name string + args args + wantHTTPCode int + want map[string]*pb.Token + wantErr bool + }{ + { + name: "get all tokens included blacklisted", + args: args{ + req: &pb.GetTokensRequest{ + IncludeBlacklisted: true, + }, + token: token, + }, + wantHTTPCode: http.StatusOK, + want: map[string]*pb.Token{ + tokenIDs[0]: { + ClientId: tokens[0].GetClientId(), + Id: tokenIDs[0], + Name: tokens[0].GetTokenName(), + }, + tokenIDs[1]: { + ClientId: tokens[1].GetClientId(), + Id: tokenIDs[1], + Name: tokens[1].GetTokenName(), + OriginalTokenClaims: func() *structpb.Value { + v, err2 := structpb.NewValue(map[string]interface{}(claims)) + require.NoError(t, err2) + return v + }(), + }, + blacklistTokenIDs[0]: { + ClientId: blacklistedTokens[0].GetClientId(), + Id: blacklistTokenIDs[0], + Name: blacklistedTokens[0].GetTokenName(), + Blacklisted: &pb.Token_BlackListed{ + Flag: true, + }, + }, + }, + }, + { + name: "get all tokens excluded blacklisted", + args: args{ + req: &pb.GetTokensRequest{}, + token: token, + }, + wantHTTPCode: http.StatusOK, + want: map[string]*pb.Token{ + tokenIDs[0]: { + ClientId: tokens[0].GetClientId(), + Id: tokenIDs[0], + Name: tokens[0].GetTokenName(), + }, + tokenIDs[1]: { + ClientId: tokens[1].GetClientId(), + Id: tokenIDs[1], + Name: tokens[1].GetTokenName(), + OriginalTokenClaims: func() *structpb.Value { + v, err2 := structpb.NewValue(map[string]interface{}(claims)) + require.NoError(t, err2) + return v + }(), + }, + }, + }, + { + name: "get certain tokens with excluded blacklisted", + args: args{ + req: &pb.GetTokensRequest{ + IdFilter: []string{tokenIDs[1], blacklistTokenIDs[0]}, + }, + token: token, + }, + wantHTTPCode: http.StatusOK, + want: map[string]*pb.Token{ + tokenIDs[1]: { + ClientId: tokens[1].GetClientId(), + Id: tokenIDs[1], + Name: tokens[1].GetTokenName(), + OriginalTokenClaims: func() *structpb.Value { + v, err2 := structpb.NewValue(map[string]interface{}(claims)) + require.NoError(t, err2) + return v + }(), + }, + }, + }, + { + name: "get certain tokens with included blacklisted", + args: args{ + req: &pb.GetTokensRequest{ + IdFilter: []string{tokenIDs[1], blacklistTokenIDs[0]}, + IncludeBlacklisted: true, + }, + token: token, + }, + wantHTTPCode: http.StatusOK, + want: map[string]*pb.Token{ + tokenIDs[1]: { + ClientId: tokens[1].GetClientId(), + Id: tokenIDs[1], + Name: tokens[1].GetTokenName(), + OriginalTokenClaims: func() *structpb.Value { + v, err2 := structpb.NewValue(map[string]interface{}(claims)) + require.NoError(t, err2) + return v + }(), + }, + blacklistTokenIDs[0]: { + ClientId: blacklistedTokens[0].GetClientId(), + Id: blacklistTokenIDs[0], + Name: blacklistedTokens[0].GetTokenName(), + Blacklisted: &pb.Token_BlackListed{ + Flag: true, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rb := testHttp.NewRequest(http.MethodGet, m2mOauthServerTest.HTTPURI(uri.Tokens), nil) + if tt.args.token != "" { + rb = rb.AuthToken(tt.args.token) + } + if tt.args.req.GetIncludeBlacklisted() { + rb = rb.AddQuery("includeBlacklisted", "true") + } + for _, id := range tt.args.req.GetIdFilter() { + rb = rb.AddQuery("idFilter", id) + } + rb = rb.ContentType(message.AppOcfCbor.String()) + resp := testHttp.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + var got []*pb.Token + for { + var gotToken pb.Token + err := pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &gotToken) + if errors.Is(err, io.EOF) { + break + } + require.NoError(t, err) + got = append(got, &gotToken) + } + require.Len(t, got, len(tt.want)) + for _, gotToken := range got { + want, ok := tt.want[gotToken.GetId()] + require.True(t, ok) + require.Equal(t, want.GetClientId(), gotToken.GetClientId()) + require.Equal(t, want.GetId(), gotToken.GetId()) + require.Equal(t, want.GetName(), gotToken.GetName()) + if want.GetOriginalTokenClaims() != nil { + test.CheckProtobufs(t, want.GetOriginalTokenClaims(), gotToken.GetOriginalTokenClaims(), test.RequireToCheckFunc(require.Equal)) + } + require.Equal(t, want.GetBlacklisted().GetFlag(), gotToken.GetBlacklisted().GetFlag()) + } + }) + } +} diff --git a/m2m-oauth-server/service/jsonWriter.go b/m2m-oauth-server/service/http/jsonWriter.go similarity index 96% rename from m2m-oauth-server/service/jsonWriter.go rename to m2m-oauth-server/service/http/jsonWriter.go index 2f5966dbe..ecd46a57c 100644 --- a/m2m-oauth-server/service/jsonWriter.go +++ b/m2m-oauth-server/service/http/jsonWriter.go @@ -1,4 +1,4 @@ -package service +package http import ( "net/http" diff --git a/m2m-oauth-server/service/http/postToken.go b/m2m-oauth-server/service/http/postToken.go new file mode 100644 index 000000000..6f1748fa7 --- /dev/null +++ b/m2m-oauth-server/service/http/postToken.go @@ -0,0 +1,112 @@ +package http + +import ( + "net/http" + "strconv" + "strings" + + "github.com/plgd-dev/hub/v2/http-gateway/serverMux" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + "github.com/plgd-dev/kit/v2/codec/json" + "google.golang.org/grpc/codes" +) + +type postRequest struct { + ClientID string `json:"client_id"` + Secret string `json:"client_secret"` + Audience string `json:"audience"` + GrantType string `json:"grant_type"` + ClientAssertionType string `json:"client_assertion_type"` + ClientAssertion string `json:"client_assertion"` + TokenName string `json:"token_name"` + Scope string `json:"scope"` + Expiration int64 `json:"expiration"` +} + +func postFormToCreateTokenRequest(r *http.Request, createTokenRequest *pb.CreateTokenRequest) { + createTokenRequest.GrantType = r.PostFormValue(uri.GrantTypeKey) + createTokenRequest.ClientId = r.PostFormValue(uri.ClientIDKey) + audience := r.PostFormValue(uri.AudienceKey) + if audience != "" { + createTokenRequest.Audience = strings.Split(audience, " ") + } + scope := r.PostFormValue(uri.ScopeKey) + if scope != "" { + createTokenRequest.Scope = strings.Split(scope, " ") + } + createTokenRequest.ClientSecret = r.PostFormValue(uri.ClientSecretKey) + createTokenRequest.ClientAssertionType = r.PostFormValue(uri.ClientAssertionTypeKey) + createTokenRequest.ClientAssertion = r.PostFormValue(uri.ClientAssertionKey) + createTokenRequest.TokenName = r.PostFormValue(uri.TokenNameKey) + expiration := r.PostFormValue(uri.ExpirationKey) + if expiration == "" { + return + } + if expirationVal, err := strconv.ParseInt(expiration, 10, 64); err == nil { + createTokenRequest.Expiration = expirationVal + } +} + +func jsonToCreateTokenRequest(req postRequest, createTokenRequest *pb.CreateTokenRequest) { + createTokenRequest.GrantType = req.GrantType + createTokenRequest.ClientId = req.ClientID + audience := req.Audience + if audience != "" { + createTokenRequest.Audience = strings.Split(audience, " ") + } + scope := req.Scope + if scope != "" { + createTokenRequest.Scope = strings.Split(scope, " ") + } + createTokenRequest.ClientSecret = req.Secret + createTokenRequest.ClientAssertionType = req.ClientAssertionType + createTokenRequest.ClientAssertion = req.ClientAssertion + createTokenRequest.TokenName = req.TokenName + createTokenRequest.Expiration = req.Expiration +} + +func (requestHandler *RequestHandler) postToken(w http.ResponseWriter, r *http.Request) { + var createTokenRequest pb.CreateTokenRequest + const cannotCreateTokenFmt = "cannot create token: %v" + if strings.Contains(r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") { + err := r.ParseForm() + if err != nil { + serverMux.WriteError(w, pkgGrpc.ForwardErrorf(codes.InvalidArgument, cannotCreateTokenFmt, err)) + return + } + postFormToCreateTokenRequest(r, &createTokenRequest) + } else { + var req postRequest + err := json.ReadFrom(r.Body, &req) + if err != nil { + serverMux.WriteError(w, pkgGrpc.ForwardErrorf(codes.InvalidArgument, cannotCreateTokenFmt, err)) + return + } + jsonToCreateTokenRequest(req, &createTokenRequest) + } + clientID, secret, ok := r.BasicAuth() + if ok { + createTokenRequest.ClientId = clientID + createTokenRequest.ClientSecret = secret + } + grpcResp, err := requestHandler.m2mOAuthServiceServer.CreateToken(r.Context(), &createTokenRequest) + if err != nil { + serverMux.WriteError(w, pkgGrpc.ForwardErrorf(codes.InvalidArgument, cannotCreateTokenFmt, err)) + return + } + resp := map[string]interface{}{ + uri.AccessTokenKey: grpcResp.GetAccessToken(), + uri.ScopeKey: strings.Join(grpcResp.GetScope(), " "), + uri.TokenTypeKey: grpcResp.GetTokenType(), + } + if grpcResp.GetExpiresIn() > 0 { + resp[uri.ExpiresInKey] = grpcResp.GetExpiresIn() + } + + if err = jsonResponseWriter(w, resp); err != nil { + log.Errorf("failed to write response: %v", err) + } +} diff --git a/m2m-oauth-server/service/token_test.go b/m2m-oauth-server/service/http/postToken_test.go similarity index 59% rename from m2m-oauth-server/service/token_test.go rename to m2m-oauth-server/service/http/postToken_test.go index 5f20eae52..eb453d0bc 100644 --- a/m2m-oauth-server/service/token_test.go +++ b/m2m-oauth-server/service/http/postToken_test.go @@ -1,13 +1,14 @@ -package service_test +package http_test import ( "context" "fmt" "net/http" "testing" + "time" "github.com/golang-jwt/jwt/v5" - "github.com/plgd-dev/hub/v2/m2m-oauth-server/service" + oauthsigner "github.com/plgd-dev/hub/v2/m2m-oauth-server/oauthSigner" m2mOauthServerTest "github.com/plgd-dev/hub/v2/m2m-oauth-server/test" "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" "github.com/plgd-dev/hub/v2/test/config" @@ -15,7 +16,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestGetToken(t *testing.T) { +func TestPostToken(t *testing.T) { type want struct { owner interface{} existOriginalTokenClaims bool @@ -43,10 +44,13 @@ func TestGetToken(t *testing.T) { Ctx: context.Background(), ClientID: m2mOauthServerTest.ServiceOAuthClient.ID, ClientSecret: m2mOauthServerTest.GetSecret(t, m2mOauthServerTest.ServiceOAuthClient.ID), - GrantType: string(service.GrantTypeClientCredentials), + GrantType: string(oauthsigner.GrantTypeClientCredentials), Host: config.M2M_OAUTH_SERVER_HTTP_HOST, }, wantCode: http.StatusOK, + want: want{ + owner: "1", + }, }, { name: "serviceToken - postForm", @@ -54,18 +58,21 @@ func TestGetToken(t *testing.T) { Ctx: context.Background(), ClientID: m2mOauthServerTest.ServiceOAuthClient.ID, ClientSecret: m2mOauthServerTest.GetSecret(t, m2mOauthServerTest.ServiceOAuthClient.ID), - GrantType: string(service.GrantTypeClientCredentials), + GrantType: string(oauthsigner.GrantTypeClientCredentials), Host: config.M2M_OAUTH_SERVER_HTTP_HOST, PostForm: true, }, wantCode: http.StatusOK, + want: want{ + owner: "1", + }, }, { - name: "snippetServiceToken - JWT", + name: "ownerToken - JWT", args: m2mOauthServerTest.AccessTokenOptions{ Ctx: context.Background(), ClientID: m2mOauthServerTest.JWTPrivateKeyOAuthClient.ID, - GrantType: string(service.GrantTypeClientCredentials), + GrantType: string(oauthsigner.GrantTypeClientCredentials), Host: config.M2M_OAUTH_SERVER_HTTP_HOST, JWT: token, }, @@ -75,34 +82,73 @@ func TestGetToken(t *testing.T) { existOriginalTokenClaims: true, }, }, + { + name: "ownerToken with expiration- JWT", + args: m2mOauthServerTest.AccessTokenOptions{ + Ctx: context.Background(), + ClientID: m2mOauthServerTest.JWTPrivateKeyOAuthClient.ID, + GrantType: string(oauthsigner.GrantTypeClientCredentials), + Host: config.M2M_OAUTH_SERVER_HTTP_HOST, + JWT: token, + Expiration: time.Now().Add(time.Hour), + }, + wantCode: http.StatusOK, + want: want{ + owner: "1", + existOriginalTokenClaims: true, + }, + }, + { + name: "ownerToken with over time expiration- JWT", + args: m2mOauthServerTest.AccessTokenOptions{ + Ctx: context.Background(), + ClientID: m2mOauthServerTest.JWTPrivateKeyOAuthClient.ID, + GrantType: string(oauthsigner.GrantTypeClientCredentials), + Host: config.M2M_OAUTH_SERVER_HTTP_HOST, + JWT: token, + Expiration: time.Now().Add(time.Hour * 24 * 365), + }, + wantCode: http.StatusUnauthorized, + }, { name: "invalid client", args: m2mOauthServerTest.AccessTokenOptions{ Ctx: context.Background(), ClientID: "invalid client", - GrantType: string(service.GrantTypeClientCredentials), + GrantType: string(oauthsigner.GrantTypeClientCredentials), Host: config.M2M_OAUTH_SERVER_HTTP_HOST, JWT: invalidToken, }, wantCode: http.StatusUnauthorized, }, { - name: "snippetServiceToken - invalid JWT", + name: "ownerToken - invalid JWT", args: m2mOauthServerTest.AccessTokenOptions{ Ctx: context.Background(), ClientID: m2mOauthServerTest.JWTPrivateKeyOAuthClient.ID, - GrantType: string(service.GrantTypeClientCredentials), + GrantType: string(oauthsigner.GrantTypeClientCredentials), Host: config.M2M_OAUTH_SERVER_HTTP_HOST, JWT: invalidToken, }, wantCode: http.StatusUnauthorized, }, + { + name: "invalid expiration", + args: m2mOauthServerTest.AccessTokenOptions{ + Ctx: context.Background(), + ClientID: m2mOauthServerTest.JWTPrivateKeyOAuthClient.ID, + GrantType: string(oauthsigner.GrantTypeClientCredentials), + Host: config.M2M_OAUTH_SERVER_HTTP_HOST, + JWT: token, + Expiration: time.Now().Add(-time.Hour), + }, + wantCode: http.StatusUnauthorized, + }, } cfg := m2mOauthServerTest.MakeConfig(t) - fmt.Printf("cfg: %v\n", cfg) - - webTearDown := m2mOauthServerTest.SetUp(t) + cfg.OAuthSigner.Clients.Find(m2mOauthServerTest.JWTPrivateKeyOAuthClient.ID).AccessTokenLifetime = time.Hour * 24 + webTearDown := m2mOauthServerTest.New(t, cfg) defer webTearDown() for _, tt := range tests { diff --git a/m2m-oauth-server/service/http/requestHandler.go b/m2m-oauth-server/service/http/requestHandler.go new file mode 100644 index 000000000..d4679a0ff --- /dev/null +++ b/m2m-oauth-server/service/http/requestHandler.go @@ -0,0 +1,46 @@ +package http + +import ( + "context" + "fmt" + "net/http" + + "github.com/fullstorydev/grpchan/inprocgrpc" + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/plgd-dev/hub/v2/http-gateway/serverMux" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + grpcService "github.com/plgd-dev/hub/v2/m2m-oauth-server/service/grpc" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" +) + +// RequestHandler for handling incoming request +type RequestHandler struct { + config *Config + m2mOAuthServiceServer *grpcService.M2MOAuthServiceServer + mux *runtime.ServeMux +} + +// NewRequestHandler returns HTTP handler +func NewRequestHandler(config *Config, r *mux.Router, m2mOAuthServiceServer *grpcService.M2MOAuthServiceServer) (*RequestHandler, error) { + requestHandler := &RequestHandler{ + config: config, + mux: serverMux.New(), + m2mOAuthServiceServer: m2mOAuthServiceServer, + } + + r.HandleFunc(uri.OpenIDConfiguration, requestHandler.getOpenIDConfiguration).Methods(http.MethodGet) + r.HandleFunc(uri.JWKs, requestHandler.getJWKs).Methods(http.MethodGet) + r.HandleFunc(uri.Token, requestHandler.postToken).Methods(http.MethodPost) + + ch := new(inprocgrpc.Channel) + pb.RegisterM2MOAuthServiceServer(ch, m2mOAuthServiceServer) + grpcClient := pb.NewM2MOAuthServiceClient(ch) + // register grpc-proxy handler + if err := pb.RegisterM2MOAuthServiceHandlerClient(context.Background(), requestHandler.mux, grpcClient); err != nil { + return nil, fmt.Errorf("failed to register m2m-oauth-server handler: %w", err) + } + r.PathPrefix("/").Handler(requestHandler.mux) + + return requestHandler, nil +} diff --git a/m2m-oauth-server/service/http/service.go b/m2m-oauth-server/service/http/service.go new file mode 100644 index 000000000..7b599f998 --- /dev/null +++ b/m2m-oauth-server/service/http/service.go @@ -0,0 +1,70 @@ +package http + +import ( + "fmt" + "net/http" + "regexp" + + grpcService "github.com/plgd-dev/hub/v2/m2m-oauth-server/service/grpc" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpJwt "github.com/plgd-dev/hub/v2/pkg/net/http/jwt" + httpService "github.com/plgd-dev/hub/v2/pkg/net/http/service" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" + "go.opentelemetry.io/otel/trace" +) + +// Service handle HTTP request +type Service struct { + *httpService.Service + requestHandler *RequestHandler +} + +// New parses configuration and creates new Server with provided store and bus +func New(serviceName string, config Config, m2mOAuthServiceServer *grpcService.M2MOAuthServiceServer, validator *validator.Validator, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*Service, error) { + whiteList := []pkgHttpJwt.RequestMatcher{ + { + Method: http.MethodGet, + URI: regexp.MustCompile(regexp.QuoteMeta(uri.JWKs)), + }, + { + Method: http.MethodGet, + URI: regexp.MustCompile(regexp.QuoteMeta(uri.OpenIDConfiguration)), + }, + { + Method: http.MethodPost, + URI: regexp.MustCompile(regexp.QuoteMeta(uri.Token)), + }, + { + Method: http.MethodPost, + URI: regexp.MustCompile(regexp.QuoteMeta(uri.Tokens)), + }, + } + service, err := httpService.New(httpService.Config{ + HTTPConnection: config.Connection, + HTTPServer: config.Server, + ServiceName: serviceName, + AuthRules: pkgHttp.NewDefaultAuthorizationRules(uri.API), + WhiteEndpointList: whiteList, + FileWatcher: fileWatcher, + Logger: logger, + TraceProvider: tracerProvider, + Validator: validator, + }) + if err != nil { + return nil, fmt.Errorf("cannot create http service: %w", err) + } + + requestHandler, err := NewRequestHandler(&config, service.GetRouter(), m2mOAuthServiceServer) + if err != nil { + _ = service.Close() + return nil, err + } + + return &Service{ + Service: service, + requestHandler: requestHandler, + }, nil +} diff --git a/m2m-oauth-server/service/httpApi.go b/m2m-oauth-server/service/httpApi.go deleted file mode 100644 index cbb50422c..000000000 --- a/m2m-oauth-server/service/httpApi.go +++ /dev/null @@ -1,86 +0,0 @@ -package service - -import ( - "context" - "fmt" - "net/http" - "time" - - router "github.com/gorilla/mux" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/plgd-dev/go-coap/v3/pkg/cache" - "github.com/plgd-dev/go-coap/v3/pkg/runner/periodic" - "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" - "github.com/plgd-dev/hub/v2/pkg/fn" - "github.com/plgd-dev/hub/v2/pkg/fsnotify" - "github.com/plgd-dev/hub/v2/pkg/log" - kitHttp "github.com/plgd-dev/hub/v2/pkg/net/http" - pkgJwt "github.com/plgd-dev/hub/v2/pkg/security/jwt" - "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" - "go.opentelemetry.io/otel/trace" -) - -// RequestHandler for handling incoming request -type RequestHandler struct { - config *Config - authRestriction *cache.Cache[string, struct{}] - accessTokenKey interface{} - accessTokenJwkKey jwk.Key - refreshRestriction *cache.Cache[string, struct{}] - privateKeyJWTValidators map[string]*validator.Validator - logger log.Logger -} - -// NewRequestHandler factory for new RequestHandler -func NewRequestHandler(ctx context.Context, config *Config, accessTokenKey interface{}, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*RequestHandler, func(), error) { - accessTokenJwkKey, err := pkgJwt.CreateJwkKey(accessTokenKey) - var closer fn.FuncList - if err != nil { - return nil, nil, fmt.Errorf("cannot create jwk for idToken: %w", err) - } - authRestriction := cache.NewCache[string, struct{}]() - refreshRestriction := cache.NewCache[string, struct{}]() - add := periodic.New(ctx.Done(), time.Second*5) - add(func(now time.Time) bool { - authRestriction.CheckExpirations(now) - refreshRestriction.CheckExpirations(now) - return true - }) - - privateKeyJWTValidators := make(map[string]*validator.Validator, len(config.OAuthSigner.Clients)) - for _, c := range config.OAuthSigner.Clients { - if !c.JWTPrivateKey.Enabled { - continue - } - validator, err := validator.New(ctx, c.JWTPrivateKey.Authorization, fileWatcher, logger, tracerProvider) - if err != nil { - closer.Execute() - return nil, nil, fmt.Errorf("cannot create validator: %w", err) - } - privateKeyJWTValidators[c.ID] = validator - closer.AddFunc(validator.Close) - } - - return &RequestHandler{ - config: config, - authRestriction: authRestriction, - accessTokenJwkKey: accessTokenJwkKey, - accessTokenKey: accessTokenKey, - refreshRestriction: refreshRestriction, - privateKeyJWTValidators: privateKeyJWTValidators, - logger: logger, - }, closer.Execute, nil -} - -// NewHTTP returns HTTP handler -func NewHTTP(requestHandler *RequestHandler, logger log.Logger) http.Handler { - r := router.NewRouter() - r.Use(kitHttp.CreateLoggingMiddleware(kitHttp.WithLogger(logger))) - r.StrictSlash(true) - - r.HandleFunc(uri.JWKs, requestHandler.getJWKs).Methods(http.MethodGet) - r.HandleFunc(uri.OpenIDConfiguration, requestHandler.getOpenIDConfiguration).Methods(http.MethodGet) - r.HandleFunc(uri.Token, requestHandler.postToken).Methods(http.MethodPost) - - return r -} diff --git a/m2m-oauth-server/service/service.go b/m2m-oauth-server/service/service.go index f9bc2fff2..139d5dca3 100644 --- a/m2m-oauth-server/service/service.go +++ b/m2m-oauth-server/service/service.go @@ -4,82 +4,153 @@ import ( "context" "fmt" "net/http" + "time" + oauthsigner "github.com/plgd-dev/hub/v2/m2m-oauth-server/oauthSigner" + grpcService "github.com/plgd-dev/hub/v2/m2m-oauth-server/service/grpc" + httpService "github.com/plgd-dev/hub/v2/m2m-oauth-server/service/http" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/store" + storeConfig "github.com/plgd-dev/hub/v2/m2m-oauth-server/store/config" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/store/mongodb" + "github.com/plgd-dev/hub/v2/pkg/config/database" + "github.com/plgd-dev/hub/v2/pkg/fn" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" - kitNetHttp "github.com/plgd-dev/hub/v2/pkg/net/http" "github.com/plgd-dev/hub/v2/pkg/net/listener" otelClient "github.com/plgd-dev/hub/v2/pkg/opentelemetry/collector/client" + certManagerServer "github.com/plgd-dev/hub/v2/pkg/security/certManager/server" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" + "github.com/plgd-dev/hub/v2/pkg/security/openid" + "github.com/plgd-dev/hub/v2/pkg/service" + "go.opentelemetry.io/otel/trace" ) const serviceName = "m2m-oauth-server" -// Server handle HTTP request type Service struct { - server *http.Server - requestHandler *RequestHandler - listener *listener.Server + *service.Service + + store store.Store +} + +func createStore(ctx context.Context, config storeConfig.Config, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (store.Store, error) { + if config.Use != database.MongoDB { + return nil, fmt.Errorf("invalid store use('%v')", config.Use) + } + s, err := mongodb.New(ctx, config.MongoDB, fileWatcher, logger, tracerProvider) + if err != nil { + return nil, fmt.Errorf("mongodb: %w", err) + } + if config.CleanUpDeletedTokens != "" { + scheduler, err2 := NewExpiredUpdatesChecker(config.CleanUpDeletedTokens, config.ExtendCronParserBySeconds, func() { + err2 := s.DeleteTokens(ctx, time.Now()) + if err2 != nil { + log.Errorf("cannot delete expired tokens: %v", err2) + } + }) + if err2 != nil { + s.Close(ctx) + return nil, fmt.Errorf("cannot create scheduler: %w", err2) + } + s.AddCloseFunc(func() { + err2 := scheduler.Shutdown() + if err2 != nil { + log.Errorf("failed to shutdown scheduler: %w", err2) + } + }) + } + return s, nil +} + +func newHttpService(ctx context.Context, config HTTPConfig, validatorConfig validator.Config, getOpenIDConfiguration validator.GetOpenIDConfigurationFunc, tlsConfig certManagerServer.Config, ss *grpcService.M2MOAuthServiceServer, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*httpService.Service, func(), error) { + httpValidator, err := validator.New(ctx, validatorConfig, fileWatcher, logger, tracerProvider, validator.WithGetOpenIDConfiguration(getOpenIDConfiguration)) + if err != nil { + return nil, nil, fmt.Errorf("cannot create http validator: %w", err) + } + httpService, err := httpService.New(serviceName, httpService.Config{ + Connection: listener.Config{ + Addr: config.Addr, + TLS: tlsConfig, + }, + Authorization: validatorConfig, + Server: config.Server, + }, ss, httpValidator, fileWatcher, logger, tracerProvider) + if err != nil { + httpValidator.Close() + return nil, nil, fmt.Errorf("cannot create http service: %w", err) + } + return httpService, httpValidator.Close, nil +} + +func newGrpcService(ctx context.Context, config grpcService.Config, getOpenIDConfiguration validator.GetOpenIDConfigurationFunc, ss *grpcService.M2MOAuthServiceServer, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*grpcService.Service, func(), error) { + grpcValidator, err := validator.New(ctx, config.Authorization.Config, fileWatcher, logger, tracerProvider, validator.WithGetOpenIDConfiguration(getOpenIDConfiguration)) + if err != nil { + return nil, nil, fmt.Errorf("cannot create grpc validator: %w", err) + } + grpcService, err := grpcService.New(config, ss, grpcValidator, fileWatcher, logger, tracerProvider) + if err != nil { + grpcValidator.Close() + return nil, nil, fmt.Errorf("cannot create grpc service: %w", err) + } + return grpcService, grpcValidator.Close, nil } -// New parses configuration and creates new Server with provided store and bus func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logger log.Logger) (*Service, error) { - ctx, cancel := context.WithCancel(ctx) otelClient, err := otelClient.New(ctx, config.Clients.OpenTelemetryCollector.Config, serviceName, fileWatcher, logger) if err != nil { - cancel() return nil, fmt.Errorf("cannot create open telemetry collector client: %w", err) } - otelClient.AddCloseFunc(cancel) + var closerFn fn.FuncList + closerFn.AddFunc(otelClient.Close) tracerProvider := otelClient.GetTracerProvider() - listener, err := listener.New(config.APIs.HTTP.Connection, fileWatcher, logger) - if err != nil { - otelClient.Close() - return nil, fmt.Errorf("cannot create http server: %w", err) - } - listener.AddCloseFunc(otelClient.Close) - closeListener := func() { - if errC := listener.Close(); errC != nil { - logger.Errorf("cannot close listener: %w", errC) + getOpenIDCfg := func(ctx context.Context, c *http.Client, authority string) (openid.Config, error) { + if authority == config.OAuthSigner.GetAuthority() { + return httpService.GetOpenIDConfiguration(config.OAuthSigner.GetDomain()), nil } + return openid.GetConfiguration(ctx, c, authority) } - accessTokenPrivateKeyI, err := LoadPrivateKey(config.OAuthSigner.PrivateKeyFile) + db, err := createStore(ctx, config.Clients.Storage, fileWatcher, logger, tracerProvider) if err != nil { - closeListener() - return nil, fmt.Errorf("cannot load private privateKeyFile(%v): %w", config.OAuthSigner.PrivateKeyFile, err) + closerFn.Execute() + return nil, fmt.Errorf("cannot create store: %w", err) } + closerFn.AddFunc(func() { + if errC := db.Close(ctx); errC != nil { + log.Errorf("failed to close store: %w", errC) + } + }) - requestHandler, closeHandler, err := NewRequestHandler(ctx, &config, accessTokenPrivateKeyI, fileWatcher, logger, tracerProvider) + signer, err := oauthsigner.New(ctx, config.OAuthSigner, getOpenIDCfg, fileWatcher, logger, tracerProvider) if err != nil { - closeListener() - return nil, fmt.Errorf("cannot create request handler: %w", err) - } - listener.AddCloseFunc(closeHandler) - - httpServer := http.Server{ - Handler: kitNetHttp.OpenTelemetryNewHandler(NewHTTP(requestHandler, logger), serviceName, tracerProvider), - ReadTimeout: config.APIs.HTTP.Server.ReadTimeout, - ReadHeaderTimeout: config.APIs.HTTP.Server.ReadHeaderTimeout, - WriteTimeout: config.APIs.HTTP.Server.WriteTimeout, - IdleTimeout: config.APIs.HTTP.Server.IdleTimeout, + closerFn.Execute() + return nil, fmt.Errorf("cannot create oauth signer: %w", err) } + closerFn.AddFunc(signer.Close) - server := Service{ - server: &httpServer, - requestHandler: requestHandler, - listener: listener, - } + m2mOAuthService := grpcService.NewM2MOAuthServerServer(db, signer, logger) - return &server, nil -} + grpcService, grpcServiceClose, err := newGrpcService(ctx, config.APIs.GRPC, getOpenIDCfg, m2mOAuthService, fileWatcher, logger, tracerProvider) + if err != nil { + closerFn.Execute() + return nil, err + } + closerFn.AddFunc(grpcServiceClose) -// Serve starts the service's HTTP server and blocks -func (s *Service) Serve() error { - return s.server.Serve(s.listener) -} + httpService, httpServiceClose, err := newHttpService(ctx, config.APIs.HTTP, config.APIs.GRPC.Authorization.Config, getOpenIDCfg, config.APIs.GRPC.TLS, + m2mOAuthService, fileWatcher, logger, tracerProvider) + if err != nil { + grpcService.Close() + closerFn.Execute() + return nil, err + } + closerFn.AddFunc(httpServiceClose) -// Shutdown ends serving -func (s *Service) Close() error { - return s.server.Shutdown(context.Background()) + s := service.New(grpcService, httpService) + s.AddCloseFunc(closerFn.Execute) + return &Service{ + Service: s, + store: db, + }, nil } diff --git a/m2m-oauth-server/service/service_test.go b/m2m-oauth-server/service/service_test.go new file mode 100644 index 000000000..98227d661 --- /dev/null +++ b/m2m-oauth-server/service/service_test.go @@ -0,0 +1,22 @@ +package service_test + +import ( + "context" + "fmt" + "testing" + + "github.com/plgd-dev/hub/v2/m2m-oauth-server/test" + "github.com/plgd-dev/hub/v2/test/config" + testService "github.com/plgd-dev/hub/v2/test/service" +) + +func TestService(t *testing.T) { + cfg := test.MakeConfig(t) + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + webTearDown := testService.SetUp(ctx, t, testService.WithM2MOAuthConfig(cfg)) + defer webTearDown() + + fmt.Printf("cfg: %v\n", cfg.String()) +} diff --git a/m2m-oauth-server/service/token.go b/m2m-oauth-server/service/token.go deleted file mode 100644 index dfdfb28cd..000000000 --- a/m2m-oauth-server/service/token.go +++ /dev/null @@ -1,349 +0,0 @@ -package service - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - "time" - - goJwt "github.com/golang-jwt/jwt/v5" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" - "github.com/plgd-dev/hub/v2/pkg/log" - pkgJwt "github.com/plgd-dev/hub/v2/pkg/security/jwt" - "github.com/plgd-dev/kit/v2/codec/json" -) - -func setKeyError(key string, err error) error { - return fmt.Errorf("failed to set %v: %w", key, err) -} - -func setKeyErrorExt(key, info interface{}, err error) error { - return fmt.Errorf("failed to set %v('%v'): %w", key, info, err) -} - -func makeAccessToken(clientCfg *Client, tokenReq tokenRequest, issuedAt, expires time.Time) (jwt.Token, error) { - token := jwt.New() - - sub := getSubject(clientCfg, tokenReq) - if err := token.Set(jwt.SubjectKey, sub); err != nil { - return nil, setKeyError(jwt.SubjectKey, err) - } - if err := token.Set(jwt.AudienceKey, tokenReq.host); err != nil { - return nil, setKeyError(jwt.AudienceKey, err) - } - if err := token.Set(jwt.IssuedAtKey, issuedAt); err != nil { - return nil, setKeyError(jwt.IssuedAtKey, err) - } - if !expires.IsZero() { - if err := token.Set(jwt.ExpirationKey, expires); err != nil { - return nil, setKeyError(jwt.ExpirationKey, err) - } - } - if err := token.Set(uri.ScopeKey, tokenReq.scopes); err != nil { - return nil, setKeyError(uri.ScopeKey, err) - } - if err := token.Set(uri.ClientIDKey, clientCfg.ID); err != nil { - return nil, setKeyError(uri.ClientIDKey, err) - } - if err := token.Set(jwt.IssuerKey, tokenReq.host); err != nil { - return nil, setKeyError(jwt.IssuerKey, err) - } - if err := setDeviceIDClaim(token, tokenReq); err != nil { - return nil, err - } - if err := setOwnerClaim(token, tokenReq); err != nil { - return nil, err - } - if err := setOriginTokenClaims(token, tokenReq); err != nil { - return nil, err - } - - for k, v := range clientCfg.InsertTokenClaims { - if _, ok := token.Get(k); ok { - continue - } - if err := token.Set(k, v); err != nil { - return nil, setKeyErrorExt(k, v, err) - } - } - - return token, nil -} - -func getSubject(clientCfg *Client, tokenReq tokenRequest) string { - if tokenReq.subject != "" { - return tokenReq.subject - } - if tokenReq.owner != "" { - return tokenReq.owner - } - return clientCfg.ID -} - -func setDeviceIDClaim(token jwt.Token, tokenReq tokenRequest) error { - if tokenReq.deviceID != "" && tokenReq.deviceIDClaim != "" { - return token.Set(tokenReq.deviceIDClaim, tokenReq.deviceID) - } - return nil -} - -func setOwnerClaim(token jwt.Token, tokenReq tokenRequest) error { - if tokenReq.owner != "" && tokenReq.ownerClaim != "" { - return token.Set(tokenReq.ownerClaim, tokenReq.owner) - } - return nil -} - -func setOriginTokenClaims(token jwt.Token, tokenReq tokenRequest) error { - if len(tokenReq.originalTokenClaims) > 0 { - return token.Set(uri.OriginalTokenClaims, tokenReq.originalTokenClaims) - } - return nil -} - -func makeJWTPayload(key interface{}, jwkKey jwk.Key, data []byte) ([]byte, error) { - hdr := jws.NewHeaders() - if err := hdr.Set(jws.TypeKey, `JWT`); err != nil { - return nil, setKeyError(jws.TypeKey, err) - } - if err := hdr.Set(jws.KeyIDKey, jwkKey.KeyID()); err != nil { - return nil, setKeyError(jws.KeyIDKey, err) - } - - payload, err := jws.Sign(data, jws.WithKey(jwkKey.Algorithm(), key, jws.WithProtectedHeaders(hdr))) - if err != nil { - return nil, fmt.Errorf("failed to create UserToken: %w", err) - } - return payload, nil -} - -func generateAccessToken(clientCfg *Client, tokenReq tokenRequest, key interface{}, jwkKey jwk.Key) (string, time.Time, error) { - now := time.Now() - var expires time.Time - if clientCfg.AccessTokenLifetime != 0 { - expires = now.Add(clientCfg.AccessTokenLifetime) - } - token, err := makeAccessToken(clientCfg, tokenReq, now, expires) - if err != nil { - return "", time.Time{}, fmt.Errorf("failed to make token: %w", err) - } - - buf, err := json.Encode(token) - if err != nil { - return "", time.Time{}, fmt.Errorf("failed to encode token: %w", err) - } - - payload, err := makeJWTPayload(key, jwkKey, buf) - if err != nil { - return "", time.Time{}, fmt.Errorf("failed to make payload: %w", err) - } - return string(payload), expires, nil -} - -type tokenRequest struct { - ClientID string `json:"client_id"` - Secret string `json:"client_secret"` - Audience string `json:"audience"` - GrantType GrantType `json:"grant_type"` - ClientAssertionType string `json:"client_assertion_type"` - ClientAssertion string `json:"client_assertion"` - - deviceID string `json:"-"` - owner string `json:"-"` - subject string `json:"-"` - host string `json:"-"` - scopes string `json:"-"` - ownerClaim string `json:"-"` - deviceIDClaim string `json:"-"` - tokenType AccessTokenType `json:"-"` - originalTokenClaims goJwt.MapClaims `json:"-"` -} - -func (requestHandler *RequestHandler) getDomain() string { - return "https://" + requestHandler.config.OAuthSigner.Domain -} - -func (requestHandler *RequestHandler) postToken(w http.ResponseWriter, r *http.Request) { - tokenReq := tokenRequest{ - host: requestHandler.getDomain(), - tokenType: AccessTokenType_JWT, - } - - if strings.Contains(r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") { - err := r.ParseForm() - if err != nil { - writeError(w, err, http.StatusBadRequest) - return - } - tokenReq.GrantType = GrantType(r.PostFormValue(uri.GrantTypeKey)) - tokenReq.ClientID = r.PostFormValue(uri.ClientIDKey) - tokenReq.Audience = r.PostFormValue(uri.AudienceKey) - tokenReq.Secret = r.PostFormValue(uri.ClientSecretKey) - tokenReq.ClientAssertionType = r.PostFormValue(uri.ClientAssertionTypeKey) - tokenReq.ClientAssertion = r.PostFormValue(uri.ClientAssertionKey) - } else { - err := json.ReadFrom(r.Body, &tokenReq) - if err != nil { - writeError(w, err, http.StatusBadRequest) - return - } - } - clientID, secret, ok := r.BasicAuth() - if ok { - tokenReq.ClientID = clientID - tokenReq.Secret = secret - } - requestHandler.processResponse(r.Context(), w, tokenReq) -} - -func sliceContains[T comparable](s []T, sub []T) bool { - // sub must be non-empty - if len(s) > 0 && len(sub) == 0 { - return false - } - check := make(map[T]struct{}, len(sub)) - for _, e := range sub { - check[e] = struct{}{} - } - for _, e := range s { - delete(check, e) - } - return len(check) == 0 -} - -func (requestHandler *RequestHandler) validateTokenRequest(ctx context.Context, clientCfg *Client, tokenReq *tokenRequest) error { - if err := validateGrantType(clientCfg, tokenReq); err != nil { - return err - } - if err := validateClient(clientCfg, tokenReq); err != nil { - return err - } - if err := validateClientAssertionType(clientCfg, tokenReq); err != nil { - return err - } - if err := requestHandler.validateClientAssertion(ctx, tokenReq); err != nil { - return err - } - if err := validateAudience(clientCfg, tokenReq); err != nil { - return err - } - - return nil -} - -func validateClient(clientCfg *Client, tokenReq *tokenRequest) error { - if clientCfg == nil { - return fmt.Errorf("client(%v) not found", tokenReq.ClientID) - } - if clientCfg.secret != "" && !clientCfg.JWTPrivateKey.Enabled && clientCfg.secret != tokenReq.Secret { - return errors.New("invalid client secret") - } - return nil -} - -func validateGrantType(clientCfg *Client, tokenReq *tokenRequest) error { - // clientCfg.AllowedGrantTypes is always non-empty - if !sliceContains(clientCfg.AllowedGrantTypes, []GrantType{tokenReq.GrantType}) { - return fmt.Errorf("invalid grant type(%v)", tokenReq.GrantType) - } - return nil -} - -func validateAudience(clientCfg *Client, tokenReq *tokenRequest) error { - var audiences []string - if tokenReq.Audience != "" { - audiences = []string{tokenReq.Audience} - } - if !sliceContains(clientCfg.AllowedAudiences, audiences) { - return fmt.Errorf("invalid audience(%v)", tokenReq.Audience) - } - return nil -} - -func validateClientAssertionType(clientCfg *Client, tokenReq *tokenRequest) error { - if tokenReq.ClientAssertionType != "" && clientCfg.JWTPrivateKey.Enabled && tokenReq.ClientAssertionType != uri.ClientAssertionTypeJWT { - return errors.New("invalid client assertion type") - } - return nil -} - -func (requestHandler *RequestHandler) validateClientAssertion(ctx context.Context, tokenReq *tokenRequest) error { - if tokenReq.ClientAssertionType == "" { - return nil - } - v, ok := requestHandler.privateKeyJWTValidators[tokenReq.ClientID] - if !ok { - return errors.New("invalid client assertion") - } - token, err := v.GetParser().ParseWithContext(ctx, tokenReq.ClientAssertion) - if err != nil { - return fmt.Errorf("invalid client assertion: %w", err) - } - tokenReq.originalTokenClaims = token - claims := pkgJwt.Claims(token) - owner, err := claims.GetOwner(requestHandler.config.OAuthSigner.OwnerClaim) - if err != nil { - return fmt.Errorf("invalid client assertion - claim owner: %w", err) - } - tokenReq.owner = owner - sub, err := claims.GetSubject() - if err != nil { - return fmt.Errorf("invalid client assertion - claim sub: %w", err) - } - tokenReq.subject = sub - if requestHandler.config.OAuthSigner.DeviceIDClaim == "" { - return nil - } - deviceID, err := claims.GetDeviceID(requestHandler.config.OAuthSigner.DeviceIDClaim) - if err == nil { - tokenReq.deviceID = deviceID - } - return nil -} - -func (requestHandler *RequestHandler) processResponse(ctx context.Context, w http.ResponseWriter, tokenReq tokenRequest) { - clientCfg := requestHandler.config.OAuthSigner.Clients.Find(tokenReq.ClientID) - if clientCfg == nil { - requestHandler.logger.Errorf("client(%v) not found - sending unauthorized", tokenReq.ClientID) - writeError(w, errors.New("invalid client"), http.StatusUnauthorized) - return - } - if err := requestHandler.validateTokenRequest(ctx, clientCfg, &tokenReq); err != nil { - requestHandler.logger.Errorf("failed to validate token request - sending unauthorized: %w", err) - writeError(w, errors.New("invalid client"), http.StatusUnauthorized) - return - } - - tokenReq.scopes = strings.Join(clientCfg.AllowedScopes, " ") - tokenReq.deviceIDClaim = requestHandler.config.OAuthSigner.DeviceIDClaim - tokenReq.ownerClaim = requestHandler.config.OAuthSigner.OwnerClaim - - accessToken, accessTokenExpires, err := generateAccessToken( - clientCfg, - tokenReq, - requestHandler.accessTokenKey, - requestHandler.accessTokenJwkKey) - if err != nil { - writeError(w, err, http.StatusInternalServerError) - return - } - - resp := map[string]interface{}{ - uri.AccessTokenKey: accessToken, - uri.ScopeKey: tokenReq.scopes, - "token_type": "Bearer", - } - if !accessTokenExpires.IsZero() { - resp["expires_in"] = int64(time.Until(accessTokenExpires).Seconds()) - } - - if err = jsonResponseWriter(w, resp); err != nil { - log.Errorf("failed to write response: %v", err) - return - } -} diff --git a/m2m-oauth-server/service/writeError.go b/m2m-oauth-server/service/writeError.go deleted file mode 100644 index 1c70693f9..000000000 --- a/m2m-oauth-server/service/writeError.go +++ /dev/null @@ -1,22 +0,0 @@ -package service - -import ( - netHttp "net/http" - - "github.com/plgd-dev/hub/v2/pkg/log" - "github.com/plgd-dev/kit/v2/codec/json" -) - -func errToJsonRes(err error) map[string]string { - return map[string]string{"err": err.Error()} -} - -func writeError(w netHttp.ResponseWriter, err error, status int) { - if err == nil { - w.WriteHeader(netHttp.StatusNoContent) - return - } - log.Errorf("%v", err) - b, _ := json.Encode(errToJsonRes(err)) - netHttp.Error(w, string(b), status) -} diff --git a/m2m-oauth-server/store/config/config.go b/m2m-oauth-server/store/config/config.go new file mode 100644 index 000000000..f315cbf6c --- /dev/null +++ b/m2m-oauth-server/store/config/config.go @@ -0,0 +1,44 @@ +package config + +import ( + "fmt" + "time" + + "github.com/go-co-op/gocron/v2" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/store/cqldb" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/store/mongodb" + "github.com/plgd-dev/hub/v2/pkg/config/database" + "github.com/plgd-dev/hub/v2/pkg/log" +) + +type Config struct { + CleanUpDeletedTokens string `yaml:"cleanUpDeletedTokens" json:"cleanUpDeletedTokens"` + ExtendCronParserBySeconds bool `yaml:"-" json:"-"` + database.Config[*mongodb.Config, *cqldb.Config] `yaml:",inline" json:",inline"` +} + +func (c *Config) Validate() error { + if err := c.Config.Validate(); err != nil { + return err + } + if c.CleanUpDeletedTokens == "" { + return nil + } + s, err := gocron.NewScheduler(gocron.WithLocation(time.Local)) //nolint:gosmopolitan + if err != nil { + return fmt.Errorf("cannot create cron job: %w", err) + } + defer func() { + if errS := s.Shutdown(); errS != nil { + log.Errorf("failed to shutdown cron job: %w", errS) + } + }() + _, err = s.NewJob(gocron.CronJob(c.CleanUpDeletedTokens, c.ExtendCronParserBySeconds), + gocron.NewTask(func() { + // do nothing + })) + if err != nil { + return fmt.Errorf("cleanUpExpiredUpdates('%v') - %w", c.CleanUpDeletedTokens, err) + } + return nil +} diff --git a/m2m-oauth-server/store/config/config_test.go b/m2m-oauth-server/store/config/config_test.go new file mode 100644 index 000000000..55245459c --- /dev/null +++ b/m2m-oauth-server/store/config/config_test.go @@ -0,0 +1,50 @@ +package config_test + +import ( + "testing" + + "github.com/plgd-dev/hub/v2/m2m-oauth-server/store/config" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/test" + "github.com/stretchr/testify/require" +) + +func TestConfig(t *testing.T) { + tests := []struct { + name string + cfg config.Config + wantErr bool + }{ + { + name: "valid", + cfg: test.MakeStoreConfig(), + }, + { + name: "valid - no cron", + cfg: func() config.Config { + cfg := test.MakeStoreConfig() + cfg.CleanUpDeletedTokens = "" + return cfg + }(), + }, + { + name: "invalid - bad cron expression", + cfg: func() config.Config { + cfg := test.MakeStoreConfig() + cfg.CleanUpDeletedTokens = "bad" + return cfg + }(), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/m2m-oauth-server/store/cqldb/config.go b/m2m-oauth-server/store/cqldb/config.go new file mode 100644 index 000000000..9ef1fcdd7 --- /dev/null +++ b/m2m-oauth-server/store/cqldb/config.go @@ -0,0 +1,15 @@ +package cqldb + +import ( + "github.com/plgd-dev/hub/v2/pkg/cqldb" +) + +// Config provides Mongo DB configuration options +type Config struct { + Embedded cqldb.Config `yaml:",inline" json:",inline"` + Table string `yaml:"table" json:"table"` +} + +func (c *Config) Validate() error { + return c.Embedded.Validate() +} diff --git a/m2m-oauth-server/store/cqldb/store.go b/m2m-oauth-server/store/cqldb/store.go new file mode 100644 index 000000000..2b608bf94 --- /dev/null +++ b/m2m-oauth-server/store/cqldb/store.go @@ -0,0 +1,9 @@ +package cqldb + +import ( + "github.com/plgd-dev/hub/v2/pkg/cqldb" +) + +type Store struct { + *cqldb.Store +} diff --git a/m2m-oauth-server/store/cqldb/tokens.go b/m2m-oauth-server/store/cqldb/tokens.go new file mode 100644 index 000000000..26bc183f1 --- /dev/null +++ b/m2m-oauth-server/store/cqldb/tokens.go @@ -0,0 +1,24 @@ +package cqldb + +import ( + "context" + + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/store" +) + +func (s *Store) CreateToken(context.Context, *pb.Token) (*pb.Token, error) { + return nil, store.ErrNotSupported +} + +func (s *Store) GetTokens(context.Context, string, *pb.GetTokensRequest, store.ProcessTokens) error { + return store.ErrNotSupported +} + +func (s *Store) DeleteTokens(context.Context) error { + return store.ErrNotSupported +} + +func (s *Store) BlacklistTokens(context.Context, string, *pb.BlacklistTokensRequest) (*pb.BlacklistTokensResponse, error) { + return nil, store.ErrNotSupported +} diff --git a/m2m-oauth-server/store/mongodb/config.go b/m2m-oauth-server/store/mongodb/config.go new file mode 100644 index 000000000..8034a68c7 --- /dev/null +++ b/m2m-oauth-server/store/mongodb/config.go @@ -0,0 +1,13 @@ +package mongodb + +import ( + pkgMongo "github.com/plgd-dev/hub/v2/pkg/mongodb" +) + +type Config struct { + Mongo pkgMongo.Config `yaml:",inline"` +} + +func (c *Config) Validate() error { + return c.Mongo.Validate() +} diff --git a/m2m-oauth-server/store/mongodb/store.go b/m2m-oauth-server/store/mongodb/store.go new file mode 100644 index 000000000..bca87a192 --- /dev/null +++ b/m2m-oauth-server/store/mongodb/store.go @@ -0,0 +1,64 @@ +package mongodb + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-multierror" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgMongo "github.com/plgd-dev/hub/v2/pkg/mongodb" + "github.com/plgd-dev/hub/v2/pkg/security/certManager/client" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.opentelemetry.io/otel/trace" +) + +type Store struct { + *pkgMongo.Store +} + +const ( + tokensCol = "tokens" +) + +var idOwnerIndex = mongo.IndexModel{ + Keys: bson.D{ + {Key: "_id", Value: 1}, + {Key: pb.OwnerKey, Value: 1}, + }, +} + +func New(ctx context.Context, cfg *Config, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*Store, error) { + certManager, err := client.New(cfg.Mongo.TLS, fileWatcher, logger) + if err != nil { + return nil, fmt.Errorf("could not create cert manager: %w", err) + } + + m, err := pkgMongo.NewStoreWithCollections(ctx, &cfg.Mongo, certManager.GetTLSConfig(), tracerProvider, map[string][]mongo.IndexModel{ + tokensCol: {idOwnerIndex}, + }) + if err != nil { + certManager.Close() + return nil, err + } + s := Store{Store: m} + s.SetOnClear(s.clearDatabases) + s.AddCloseFunc(certManager.Close) + return &s, nil +} + +func (s *Store) clearDatabases(ctx context.Context) error { + var errors *multierror.Error + collections := []string{tokensCol} + for _, collection := range collections { + err := s.Collection(collection).Drop(ctx) + errors = multierror.Append(errors, err) + } + return errors.ErrorOrNil() +} + +func (s *Store) Close(ctx context.Context) error { + return s.Store.Close(ctx) +} diff --git a/m2m-oauth-server/store/mongodb/tokens.go b/m2m-oauth-server/store/mongodb/tokens.go new file mode 100644 index 000000000..c4c0f3eb1 --- /dev/null +++ b/m2m-oauth-server/store/mongodb/tokens.go @@ -0,0 +1,168 @@ +package mongodb + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/hashicorp/go-multierror" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/store" + "github.com/plgd-dev/hub/v2/pkg/mongodb" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func (s *Store) CreateToken(ctx context.Context, owner string, token *pb.Token) (*pb.Token, error) { + if token.GetOwner() == "" { + token.Owner = owner + } + if token.GetId() == "" { + token.Id = uuid.NewString() + } + if owner != token.GetOwner() { + return nil, store.ErrInvalidArgument + } + err := token.Validate() + if err != nil { + return nil, err + } + _, err = s.Store.Collection(tokensCol).InsertOne(ctx, token) + if err != nil { + return nil, err + } + return token, nil +} + +func toFilter(owner string, req *pb.GetTokensRequest) (filter bson.D, hint interface{}) { + setIdOwnerHint := true + if len(req.GetIdFilter()) > 0 { + filter = append(filter, bson.E{Key: "_id", Value: bson.M{mongodb.In: req.GetIdFilter()}}) + } else { + setIdOwnerHint = false + } + if owner != "" { + filter = append(filter, bson.E{Key: pb.OwnerKey, Value: owner}) + } else { + setIdOwnerHint = false + } + if !req.GetIncludeBlacklisted() { + setIdOwnerHint = false + filter = append(filter, + bson.E{ + Key: mongodb.Or, Value: bson.A{ + bson.M{ + pb.BlackListedFlagKey: bson.M{ + mongodb.Exists: false, + }, + }, + bson.M{ + pb.BlackListedFlagKey: false, + }, + }, + }) + } + if setIdOwnerHint { + hint = idOwnerIndex.Keys + } + return filter, hint +} + +func processCursor[T any](ctx context.Context, cr *mongo.Cursor, process store.Process[T]) error { + var errors *multierror.Error + iter := store.MongoIterator[T]{ + Cursor: cr, + } + for { + var stored T + if !iter.Next(ctx, &stored) { + break + } + err := process(&stored) + if err != nil { + errors = multierror.Append(errors, err) + break + } + } + errors = multierror.Append(errors, iter.Err()) + errClose := cr.Close(ctx) + errors = multierror.Append(errors, errClose) + return errors.ErrorOrNil() +} + +func (s *Store) GetTokens(ctx context.Context, owner string, req *pb.GetTokensRequest, process store.ProcessTokens) error { + if owner == "" { + return store.ErrInvalidArgument + } + filter, hint := toFilter(owner, req) + opts := options.Find() + if hint != nil { + opts.SetHint(hint) + } + cur, err := s.Store.Collection(tokensCol).Find(ctx, filter, opts) + if err != nil { + return err + } + return processCursor(ctx, cur, process) +} + +func (s *Store) DeleteTokens(ctx context.Context, now time.Time) error { + deleteFilter := bson.D{ + {Key: pb.ExpirationKey, Value: bson.M{"$lt": now.Unix()}}, + {Key: pb.ExpirationKey, Value: bson.M{"$gt": int64(0)}}, + {Key: pb.BlackListedFlagKey, Value: true}, + } + _, err := s.Store.Collection(tokensCol).DeleteMany(ctx, deleteFilter) + return err +} + +func (s *Store) BlacklistTokens(ctx context.Context, owner string, req *pb.BlacklistTokensRequest) (*pb.BlacklistTokensResponse, error) { + if owner == "" { + return nil, store.ErrInvalidArgument + } + filter := bson.D{ + {Key: pb.OwnerKey, Value: owner}, + { + Key: mongodb.Or, Value: bson.A{ + bson.M{ + pb.ExpirationKey: bson.M{"$gte": time.Now().Unix()}, + }, + bson.M{ + pb.ExpirationKey: bson.M{mongodb.Exists: false}, + }, + bson.M{ + pb.ExpirationKey: int64(0), + }, + }, + }, + { + Key: mongodb.Or, Value: bson.A{ + bson.M{pb.BlackListedFlagKey: false}, + bson.M{pb.BlackListedFlagKey: bson.M{mongodb.Exists: false}}, + }, + }, + } + if len(req.GetIdFilter()) > 0 { + filter = append(filter, bson.E{Key: "_id", Value: bson.M{mongodb.In: req.GetIdFilter()}}) + } + blacklisted := pb.Token_BlackListed{ + Flag: true, + Timestamp: time.Now().Unix(), + } + + update := bson.D{ + { + Key: mongodb.Set, Value: bson.M{ + pb.BlackListedKey: &blacklisted, + }, + }, + } + ret, err := s.Store.Collection(tokensCol).UpdateMany(ctx, filter, update) + if err != nil { + return nil, err + } + return &pb.BlacklistTokensResponse{ + Count: ret.MatchedCount, + }, nil +} diff --git a/m2m-oauth-server/store/mongodb/tokens_test.go b/m2m-oauth-server/store/mongodb/tokens_test.go new file mode 100644 index 000000000..f8ecb88c5 --- /dev/null +++ b/m2m-oauth-server/store/mongodb/tokens_test.go @@ -0,0 +1,311 @@ +package mongodb_test + +import ( + "context" + "testing" + "time" + + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/test" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" +) + +func TestGetTokens(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + expiration := time.Now().Add(time.Minute * 10).Unix() + + // Set the owner and request parameters + owner := "testOwner" + tokens := []*pb.Token{ + { + Id: "token1", + Owner: owner, + Version: 0, + Name: "name1", + IssuedAt: time.Now().Unix(), + ClientId: "client1", + Expiration: expiration, + }, + { + Id: "token2", + Owner: owner, + Version: 0, + Name: "name2", + IssuedAt: time.Now().Unix(), + ClientId: "client1", + Blacklisted: &pb.Token_BlackListed{ + Flag: true, + Timestamp: time.Now().Unix(), + }, + }, + } + + type args struct { + ctx context.Context + owner string + req *pb.GetTokensRequest + } + + tests := []struct { + name string + args args + want []*pb.Token + }{ + { + name: "all tokens", + args: args{ + ctx: ctx, + owner: owner, + req: &pb.GetTokensRequest{}, + }, + want: []*pb.Token{ + tokens[0], + }, + }, + { + name: "all tokens including blacklisted", + args: args{ + ctx: ctx, + owner: owner, + req: &pb.GetTokensRequest{ + IncludeBlacklisted: true, + }, + }, + want: tokens, + }, + { + name: "certain token", + args: args{ + ctx: ctx, + owner: owner, + req: &pb.GetTokensRequest{ + IdFilter: []string{"token2"}, + IncludeBlacklisted: true, + }, + }, + want: []*pb.Token{ + tokens[1], + }, + }, + { + name: "all tokens another owner", + args: args{ + ctx: ctx, + owner: "anotherOwner", + req: &pb.GetTokensRequest{}, + }, + want: nil, + }, + } + + for _, token := range tokens { + _, err := s.CreateToken(ctx, owner, token) + require.NoError(t, err) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := make(map[string]*pb.Token) + // Define a mock process function + process := func(token *pb.Token) error { + result[token.GetId()] = token + return nil + } + + // Call the GetTokens method + err := s.GetTokens(tt.args.ctx, tt.args.owner, tt.args.req, process) + require.NoError(t, err) + require.Len(t, result, len(tt.want)) + for _, token := range tt.want { + require.Contains(t, result, token.GetId()) + require.Equal(t, token.GetExpiration(), result[token.GetId()].GetExpiration()) + require.Equal(t, token.GetIssuedAt(), result[token.GetId()].GetIssuedAt()) + require.Equal(t, token.GetClientId(), result[token.GetId()].GetClientId()) + require.Equal(t, token.GetOwner(), result[token.GetId()].GetOwner()) + require.Equal(t, token.GetVersion(), result[token.GetId()].GetVersion()) + require.Equal(t, token.GetName(), result[token.GetId()].GetName()) + require.Equal(t, token.GetBlacklisted().GetFlag(), result[token.GetId()].GetBlacklisted().GetFlag()) + require.Equal(t, token.GetBlacklisted().GetTimestamp(), result[token.GetId()].GetBlacklisted().GetTimestamp()) + } + }) + } +} + +func TestBlacklistTokens(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + owner := "testOwner" + tokens := []*pb.Token{ + { + Id: "token1", + Owner: owner, + Version: 0, + Name: "name1", + IssuedAt: time.Now().Unix(), + ClientId: "client1", + }, + { + Id: "token2", + Owner: owner, + Version: 0, + Name: "name2", + IssuedAt: time.Now().Unix(), + ClientId: "client1", + }, + { + Id: "token3", + Owner: owner, + Version: 0, + Name: "name3", + IssuedAt: time.Now().Unix(), + ClientId: "client1", + }, + } + + for _, token := range tokens { + _, err := s.CreateToken(ctx, owner, token) + require.NoError(t, err) + } + + req := &pb.BlacklistTokensRequest{ + IdFilter: []string{"token1", "token2"}, + } + + resp, err := s.BlacklistTokens(ctx, owner, req) + require.NoError(t, err) + require.Equal(t, int64(2), resp.GetCount()) + + blacklistedTokens := []*pb.Token{ + { + Id: "token1", + Owner: owner, + Version: 0, + Name: "name1", + IssuedAt: time.Now().Unix(), + ClientId: "client1", + Blacklisted: &pb.Token_BlackListed{ + Flag: true, + Timestamp: time.Now().Unix(), + }, + }, + { + Id: "token2", + Owner: owner, + Version: 0, + Name: "name2", + IssuedAt: time.Now().Unix(), + ClientId: "client1", + Blacklisted: &pb.Token_BlackListed{ + Flag: true, + Timestamp: time.Now().Unix(), + }, + }, + } + + for _, token := range blacklistedTokens { + storedToken := make(map[string]*pb.Token) + process := func(token *pb.Token) error { + storedToken[token.GetId()] = token + return nil + } + + err := s.GetTokens(ctx, owner, &pb.GetTokensRequest{ + IdFilter: []string{token.GetId()}, + IncludeBlacklisted: true, + }, process) + require.NoError(t, err) + require.NotNil(t, storedToken) + require.True(t, storedToken[token.GetId()].GetBlacklisted().GetFlag()) + require.Greater(t, storedToken[token.GetId()].GetBlacklisted().GetTimestamp(), int64(0)) + } +} + +func TestDeleteTokens(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + owner := "testOwner" + tokens := []*pb.Token{ + { + Id: "token1", + Owner: owner, + Version: 0, + Name: "name1", + IssuedAt: time.Now().Unix(), + ClientId: "client1", + Expiration: time.Now().Add(time.Minute * 10).Unix(), + Blacklisted: &pb.Token_BlackListed{ + Flag: true, + Timestamp: time.Now().Unix(), + }, + }, + { + Id: "token2", + Owner: owner, + Version: 0, + Name: "name2", + IssuedAt: time.Now().Unix(), + ClientId: "client1", + Expiration: time.Now().Add(time.Minute * 10).Unix(), + Blacklisted: &pb.Token_BlackListed{ + Flag: true, + Timestamp: time.Now().Add(time.Minute).Unix(), + }, + }, + { + Id: "token3", + Owner: owner, + Version: 0, + Name: "name3", + IssuedAt: time.Now().Unix(), + ClientId: "client1", + }, + } + + for _, token := range tokens { + _, err := s.CreateToken(ctx, owner, token) + require.NoError(t, err) + } + + err := s.DeleteTokens(ctx, time.Now().Add(time.Hour)) + require.NoError(t, err) + + remainingTokens := []*pb.Token{ + { + Id: "token3", + Owner: owner, + Version: 0, + Name: "name3", + IssuedAt: time.Now().Unix(), + ClientId: "client1", + }, + } + + result := make(map[string]*pb.Token) + process := func(token *pb.Token) error { + result[token.GetId()] = token + return nil + } + + err = s.GetTokens(ctx, owner, &pb.GetTokensRequest{ + IncludeBlacklisted: true, + }, process) + require.NoError(t, err) + require.Len(t, result, len(remainingTokens)) + for _, token := range remainingTokens { + require.Contains(t, result, token.GetId()) + } +} diff --git a/m2m-oauth-server/store/store.go b/m2m-oauth-server/store/store.go new file mode 100644 index 000000000..60015a071 --- /dev/null +++ b/m2m-oauth-server/store/store.go @@ -0,0 +1,59 @@ +package store + +import ( + "context" + "errors" + "time" + + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + "go.mongodb.org/mongo-driver/mongo" +) + +type Iterator[T any] interface { + Next(ctx context.Context, v *T) bool + Err() error +} + +type ( + Process[T any] func(v *T) error + ProcessTokens = Process[pb.Token] +) + +var ( + ErrNotSupported = errors.New("not supported") + ErrNotFound = errors.New("not found") + ErrNotModified = errors.New("not modified") + ErrInvalidArgument = errors.New("invalid argument") + ErrPartialDelete = errors.New("some errors occurred while deleting") +) + +type MongoIterator[T any] struct { + Cursor *mongo.Cursor +} + +func (i *MongoIterator[T]) Next(ctx context.Context, s *T) bool { + if !i.Cursor.Next(ctx) { + return false + } + err := i.Cursor.Decode(s) + return err == nil +} + +func (i *MongoIterator[T]) Err() error { + return i.Cursor.Err() +} + +type Store interface { + // CreateToken creates a new token. If the token already exists, it will throw an error. + CreateToken(ctx context.Context, owner string, token *pb.Token) (*pb.Token, error) + // GetTokens loads tokens from the database. + GetTokens(ctx context.Context, owner string, query *pb.GetTokensRequest, p ProcessTokens) error + + // DeleteTokens deletes blacklisted expired tokens from the database. + DeleteTokens(ctx context.Context, now time.Time) error + + // Set tokens as blacklisted + BlacklistTokens(ctx context.Context, owner string, req *pb.BlacklistTokensRequest) (*pb.BlacklistTokensResponse, error) + + Close(ctx context.Context) error +} diff --git a/m2m-oauth-server/swagger.yaml b/m2m-oauth-server/swagger.yaml index fb0588453..7b60de173 100644 --- a/m2m-oauth-server/swagger.yaml +++ b/m2m-oauth-server/swagger.yaml @@ -1,11 +1,24 @@ -openapi: 3.0.0 +openapi: 3.0.1 info: - title: m2m-oauth-server API - description: API documentation for m2m-oauth-server - version: 1.0.0 + title: PLGD M2M API + description: API for to manage m2m tokens in PLGD + contact: + name: plgd.dev + url: https://github.com/plgd-dev/hub + email: info@plgd.dev + license: + name: Apache License 2.0 + url: https://github.com/plgd-dev/hub/blob/v2/LICENSE + version: "1.0" +servers: +- url: / +tags: +- name: M2MOAuthService paths: /m2m-oauth-server/oauth/token: post: + tags: + - Native OAuth summary: Obtain an OAuth token description: This endpoint is used to obtain an OAuth token by providing the necessary credentials and parameters. requestBody: @@ -18,6 +31,12 @@ paths: client_id: type: string description: "The client ID." + token_name: + type: string + description: "The name of the token which will be used in the name claim." + expiration: + type: integer + description: "The requested expiration time in unit timestamp seconds from the client. If not provided, the token will use the maximum allowed by the client, or if it exceeds the maximum allowed, an error will occur." scope: type: string description: "The scopes that are requested, separated by space. Must be a subset of the allowed scopes for the client." @@ -52,9 +71,10 @@ paths: description: "The scopes granted for the token." '401': description: Unauthorized. The request requires valid user authentication. - /m2m-oauth-server/.well-known/jwks.json: get: + tags: + - Native OAuth summary: Retrieve JSON Web Key Set (JWKS) description: This endpoint retrieves the JSON Web Key Set (JWKS), which contains the public keys used to verify the JWT tokens. responses: @@ -62,9 +82,10 @@ paths: description: JSON Web Key Set retrieved successfully '404': description: JWKS not found. The requested JWKS does not exist. - /m2m-oauth-server/.well-known/openid-configuration: get: + tags: + - Native OAuth summary: Retrieve OpenID Configuration description: This endpoint retrieves the OpenID Configuration, which contains the necessary information for clients to interact with the OAuth server. responses: @@ -72,3 +93,275 @@ paths: description: OpenID Configuration retrieved successfully '404': description: OpenID Configuration not found. The requested OpenID Configuration does not exist. + /m2m-oauth-server/api/v1/blacklist: + post: + tags: + - Tokens + summary: Blacklists/revokes tokens + operationId: M2MOAuthService_BlacklistTokens + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/pbBlacklistTokensRequest' + application/protojson: + schema: + $ref: '#/components/schemas/pbBlacklistTokensRequest' + required: true + responses: + "200": + description: A successful response. + content: + application/json: + schema: + $ref: '#/components/schemas/pbBlacklistTokensResponse' + application/protojson: + schema: + $ref: '#/components/schemas/pbBlacklistTokensResponse' + default: + description: An unexpected error response. + content: + application/json: + schema: + $ref: '#/components/schemas/rpcStatus' + application/protojson: + schema: + $ref: '#/components/schemas/rpcStatus' + x-codegen-request-body-name: body + /m2m-oauth-server/api/v1/tokens: + get: + tags: + - Tokens + summary: Returns all tokens of the owner + operationId: M2MOAuthService_GetTokens + parameters: + - name: idFilter + in: query + style: form + explode: true + schema: + type: array + items: + type: string + - name: includeBlacklisted + in: query + schema: + type: boolean + responses: + "200": + description: A successful response.(streaming responses) + content: + application/json: + schema: + title: Stream result of pbToken + type: object + properties: + result: + $ref: '#/components/schemas/pbToken' + error: + $ref: '#/components/schemas/rpcStatus' + application/protojson: + schema: + title: Stream result of pbToken + type: object + properties: + result: + $ref: '#/components/schemas/pbToken' + error: + $ref: '#/components/schemas/rpcStatus' + default: + description: An unexpected error response. + content: + application/json: + schema: + $ref: '#/components/schemas/rpcStatus' + application/protojson: + schema: + $ref: '#/components/schemas/rpcStatus' + post: + tags: + - Tokens + summary: Creates a new token + operationId: M2MOAuthService_CreateToken + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/pbCreateTokenRequest' + application/protojson: + schema: + $ref: '#/components/schemas/pbCreateTokenRequest' + required: true + responses: + "200": + description: A successful response. + content: + application/json: + schema: + $ref: '#/components/schemas/pbCreateTokenResponse' + application/protojson: + schema: + $ref: '#/components/schemas/pbCreateTokenResponse' + default: + description: An unexpected error response. + content: + application/json: + schema: + $ref: '#/components/schemas/rpcStatus' + application/protojson: + schema: + $ref: '#/components/schemas/rpcStatus' + x-codegen-request-body-name: body +components: + schemas: + TokenBlackListed: + type: object + properties: + flag: + title: "Blacklisted enabled flag, if once token has been blacklisted then\ + \ it can't be unblacklisted/unrevoked" + type: boolean + timestamp: + title: Unix timestamp in s when the token has been blacklisted + type: string + format: int64 + pbBlacklistTokensRequest: + type: object + properties: + idFilter: + type: array + items: + type: string + pbBlacklistTokensResponse: + type: object + properties: + count: + type: string + format: int64 + pbCreateTokenRequest: + type: object + properties: + clientId: + title: Client ID + type: string + clientSecret: + title: Client Secret + type: string + audience: + title: Requested token Audience + type: array + items: + type: string + scope: + title: Requested token scopes + type: array + items: + type: string + expiration: + title: The requested expiration time in unit timestamp seconds from the client. If not provided, the token will use the maximum allowed by the client, or if it exceeds the maximum allowed, an error will occur. + type: string + format: int64 + clientAssertionType: + title: Client assertion type + type: string + clientAssertion: + title: Client assertion + type: string + tokenName: + title: Token name + type: string + grantType: + title: Grant type + type: string + pbCreateTokenResponse: + type: object + properties: + accessToken: + type: string + tokenType: + type: string + expiresIn: + type: string + format: int64 + scope: + type: array + items: + type: string + pbToken: + title: Tokens are deleted from DB after they are expired and blacklisted/revoked + type: object + properties: + id: + title: Token ID / jti + type: string + version: + title: Incremental version for update + type: string + format: uint64 + name: + title: User-friendly token name + type: string + owner: + title: Owner of the token + type: string + issuedAt: + title: Unix timestamp in s when the condition has been created/updated + type: string + format: int64 + audience: + title: Token Audience + type: array + items: + type: string + scope: + title: Token scopes + type: array + items: + type: string + expiration: + title: Token expiration in Unix timestamp seconds + type: string + format: int64 + clientId: + title: Client ID + type: string + originalTokenClaims: + title: Original token claims + type: object + blacklisted: + $ref: '#/components/schemas/TokenBlackListed' + subject: + title: Subject of the token + type: string + description: driven by resource change event + protobufAny: + type: object + properties: + '@type': + type: string + additionalProperties: + type: object + protobufNullValue: + type: string + description: |- + `NullValue` is a singleton enumeration to represent the null value for the + `Value` type union. + + The JSON representation for `NullValue` is JSON `null`. + + - NULL_VALUE: Null value. + default: NULL_VALUE + enum: + - NULL_VALUE + rpcStatus: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + $ref: '#/components/schemas/protobufAny' diff --git a/m2m-oauth-server/test/http.go b/m2m-oauth-server/test/http.go new file mode 100644 index 000000000..0bec01350 --- /dev/null +++ b/m2m-oauth-server/test/http.go @@ -0,0 +1,38 @@ +package test + +import ( + "bytes" + "context" + "net/http" + "testing" + + "github.com/plgd-dev/go-coap/v3/message" + grpcPb "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" + "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + testHttp "github.com/plgd-dev/hub/v2/test/http" + "github.com/stretchr/testify/require" +) + +func HTTPURI(uri string) string { + return testHttp.HTTPS_SCHEME + config.M2M_OAUTH_SERVER_HTTP_HOST + uri +} + +func BlacklistTokens(ctx context.Context, t *testing.T, tokenIDs []string, token string) { + data, err := testHttp.GetContentData(&grpcPb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, &pb.BlacklistTokensRequest{ + IdFilter: tokenIDs, + }), + }, message.AppJSON.String()) + require.NoError(t, err) + rb := testHttp.NewRequest(http.MethodPost, HTTPURI(uri.BlacklistTokens), bytes.NewReader(data)).AuthToken(token) + rb = rb.ContentType(message.AppOcfCbor.String()) + resp := testHttp.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, http.StatusOK, resp.StatusCode) +} diff --git a/m2m-oauth-server/test/service.go b/m2m-oauth-server/test/service.go new file mode 100644 index 000000000..02db193dc --- /dev/null +++ b/m2m-oauth-server/test/service.go @@ -0,0 +1,65 @@ +package test + +import ( + "context" + "time" + + storeConfig "github.com/plgd-dev/hub/v2/m2m-oauth-server/store/config" + storeCqlDB "github.com/plgd-dev/hub/v2/m2m-oauth-server/store/cqldb" + storeMongo "github.com/plgd-dev/hub/v2/m2m-oauth-server/store/mongodb" + "github.com/plgd-dev/hub/v2/pkg/config/database" + "github.com/plgd-dev/hub/v2/pkg/fsnotify" + "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/mongodb" + "github.com/plgd-dev/hub/v2/test/config" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" +) + +func MakeStoreConfig() storeConfig.Config { + return storeConfig.Config{ + // TODO: add cqldb support + // Use: config.ACTIVE_DATABASE(), + CleanUpDeletedTokens: "0 * * * *", + ExtendCronParserBySeconds: false, + Config: database.Config[*storeMongo.Config, *storeCqlDB.Config]{ + Use: database.MongoDB, + MongoDB: &storeMongo.Config{ + Mongo: mongodb.Config{ + MaxPoolSize: 16, + MaxConnIdleTime: time.Minute * 4, + URI: config.MONGODB_URI, + Database: "m2mOAuthServer", + TLS: config.MakeTLSClientConfig(), + }, + }, + CqlDB: &storeCqlDB.Config{ + Embedded: config.MakeCqlDBConfig(), + Table: "m2mOAuthServer", + }, + }, + } +} + +func NewMongoStore(t require.TestingT) (*storeMongo.Store, func()) { + cfg := MakeConfig(t) + logger := log.NewLogger(cfg.Log) + + fileWatcher, err := fsnotify.NewWatcher(logger) + require.NoError(t, err) + + ctx := context.Background() + store, err := storeMongo.New(ctx, cfg.Clients.Storage.MongoDB, fileWatcher, logger, noop.NewTracerProvider()) + require.NoError(t, err) + + cleanUp := func() { + err := store.Clear(ctx) + require.NoError(t, err) + _ = store.Close(ctx) + + err = fileWatcher.Close() + require.NoError(t, err) + } + + return store, cleanUp +} diff --git a/m2m-oauth-server/test/test.go b/m2m-oauth-server/test/test.go index 91c5da67d..b57b582a1 100644 --- a/m2m-oauth-server/test/test.go +++ b/m2m-oauth-server/test/test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + oauthsigner "github.com/plgd-dev/hub/v2/m2m-oauth-server/oauthSigner" "github.com/plgd-dev/hub/v2/m2m-oauth-server/service" "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" @@ -20,6 +21,7 @@ import ( "github.com/plgd-dev/hub/v2/pkg/log" kitNetHttp "github.com/plgd-dev/hub/v2/pkg/net/http" "github.com/plgd-dev/hub/v2/pkg/security/jwt" + "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" "github.com/plgd-dev/hub/v2/test/config" testHttp "github.com/plgd-dev/hub/v2/test/http" testOAuthUri "github.com/plgd-dev/hub/v2/test/oauth-server/uri" @@ -32,45 +34,65 @@ const ( DeviceIDClaim = testOAuthUri.DeviceIDClaimKey ) -var ServiceOAuthClient = service.Client{ +var ServiceOAuthClient = oauthsigner.Client{ ID: "serviceClient", SecretFile: "data:,serviceClientSecret", + Owner: "1", AccessTokenLifetime: 0, - AllowedGrantTypes: []service.GrantType{service.GrantTypeClientCredentials}, + AllowedGrantTypes: []oauthsigner.GrantType{oauthsigner.GrantTypeClientCredentials}, AllowedAudiences: nil, AllowedScopes: nil, InsertTokenClaims: map[string]interface{}{"hardcodedClaim": true}, } -var JWTPrivateKeyOAuthClient = service.Client{ +var JWTPrivateKeyOAuthClient = oauthsigner.Client{ ID: "JWTPrivateKeyClient", SecretFile: "data:,JWTPrivateKeyClientSecret", AccessTokenLifetime: 0, - AllowedGrantTypes: []service.GrantType{service.GrantTypeClientCredentials}, + AllowedGrantTypes: []oauthsigner.GrantType{oauthsigner.GrantTypeClientCredentials}, AllowedAudiences: nil, AllowedScopes: nil, - JWTPrivateKey: service.PrivateKeyJWTConfig{ + JWTPrivateKey: oauthsigner.PrivateKeyJWTConfig{ Enabled: true, - Authorization: config.MakeValidatorConfig(), + Authorization: MakeValidatorConfig(), }, } -var OAuthClients = service.OAuthClientsConfig{ +var OAuthClients = oauthsigner.OAuthClientsConfig{ &ServiceOAuthClient, &JWTPrivateKeyOAuthClient, } +func MakeValidatorConfig() validator.Config { + c := config.MakeValidatorConfig() + // tokens are verified by the m2m-oauth-server, so we want to disable the verification here to avoid infinite loop + // of token verification + c.TokenVerification = validator.TokenTrustVerificationConfig{ + Enabled: false, + } + return c +} + func MakeConfig(t require.TestingT) service.Config { var cfg service.Config cfg.Log = log.MakeDefaultConfig() - cfg.APIs.HTTP.Connection = config.MakeListenerConfig(config.M2M_OAUTH_SERVER_HTTP_HOST) - cfg.APIs.HTTP.Connection.TLS.ClientCertificateRequired = false + cfg.APIs.HTTP.Addr = config.M2M_OAUTH_SERVER_HTTP_HOST cfg.APIs.HTTP.Server = config.MakeHttpServerConfig() cfg.Clients.OpenTelemetryCollector = kitNetHttp.OpenTelemetryCollectorConfig{ Config: config.MakeOpenTelemetryCollectorClient(), } + cfg.APIs.GRPC = config.MakeGrpcServerConfig(config.M2M_OAUTH_SERVER_HOST) + cfg.APIs.GRPC.TLS.ClientCertificateRequired = false + cfg.APIs.GRPC.Authorization.Endpoints = append(cfg.APIs.GRPC.Authorization.Endpoints, + validator.AuthorityConfig{ + Authority: testHttp.HTTPS_SCHEME + config.M2M_OAUTH_SERVER_HTTP_HOST + uri.Base, + HTTP: config.MakeHttpClientConfig(), + }, + ) + cfg.APIs.GRPC.Authorization.Config = MakeValidatorConfig() + cfg.Clients.Storage = MakeStoreConfig() cfg.OAuthSigner.PrivateKeyFile = urischeme.URIScheme(os.Getenv("M2M_OAUTH_SERVER_PRIVATE_KEY")) cfg.OAuthSigner.Domain = config.M2M_OAUTH_SERVER_HTTP_HOST @@ -131,6 +153,7 @@ type AccessTokenOptions struct { Audience string JWT string PostForm bool + Expiration time.Time Ctx context.Context } @@ -182,6 +205,12 @@ func WithPostFrom(enabled bool) func(opts *AccessTokenOptions) { } } +func WithExpiration(expiration time.Time) func(opts *AccessTokenOptions) { + return func(opts *AccessTokenOptions) { + opts.Expiration = expiration + } +} + func WithContext(ctx context.Context) func(opts *AccessTokenOptions) { return func(opts *AccessTokenOptions) { opts.Ctx = ctx @@ -201,7 +230,7 @@ func GetAccessToken(t *testing.T, expectedCode int, opts ...func(opts *AccessTok Host: config.M2M_OAUTH_SERVER_HTTP_HOST, ClientID: ServiceOAuthClient.ID, ClientSecret: GetSecret(t, ServiceOAuthClient.ID), - GrantType: string(service.GrantTypeClientCredentials), + GrantType: string(oauthsigner.GrantTypeClientCredentials), Ctx: context.Background(), } for _, o := range opts { @@ -218,6 +247,9 @@ func GetAccessToken(t *testing.T, expectedCode int, opts ...func(opts *AccessTok reqBody[uri.ClientAssertionKey] = options.JWT reqBody[uri.ClientAssertionTypeKey] = uri.ClientAssertionTypeJWT } + if !options.Expiration.IsZero() { + reqBody[uri.ExpirationKey] = options.Expiration.Unix() + } var data []byte if options.PostForm { data = []byte(mapToURLValues(reqBody).Encode()) @@ -264,5 +296,5 @@ func GetJWTValidator(jwkURL string) *jwt.Validator { Transport: t, Timeout: time.Second * 10, } - return jwt.NewValidator(jwt.NewKeyCache(jwkURL, &client)) + return jwt.NewValidator(jwt.NewKeyCache(jwkURL, &client), log.Get()) } diff --git a/m2m-oauth-server/uri/uri.go b/m2m-oauth-server/uri/uri.go index 12705adce..349de5d32 100644 --- a/m2m-oauth-server/uri/uri.go +++ b/m2m-oauth-server/uri/uri.go @@ -5,18 +5,25 @@ import "github.com/lestrrat-go/jwx/v2/jwt" const ( ClientIDKey = "client_id" ClientSecretKey = "client_secret" + TokenNameKey = "token_name" ScopeKey = "scope" GrantTypeKey = "grant_type" + ExpirationKey = "expiration" AudienceKey = jwt.AudienceKey AccessTokenKey = "access_token" ClientAssertionTypeKey = "client_assertion_type" ClientAssertionTypeJWT = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" ClientAssertionKey = "client_assertion" + TokenTypeKey = "token_type" + ExpiresInKey = "expires_in" OriginalTokenClaims = "https://plgd.dev/originalClaims" Base = "/m2m-oauth-server" + API = Base + "/api/v1" Token = Base + "/oauth/token" JWKs = Base + "/.well-known/jwks.json" OpenIDConfiguration = Base + "/.well-known/openid-configuration" + Tokens = API + "/tokens" + BlacklistTokens = API + "/blacklist" ) diff --git a/pkg/mongodb/marshal.go b/pkg/mongodb/marshal.go index 54dbd8acb..1bf4a811d 100644 --- a/pkg/mongodb/marshal.go +++ b/pkg/mongodb/marshal.go @@ -2,68 +2,270 @@ package mongodb import ( "encoding/json" + "errors" + "fmt" + "regexp" "strconv" "strings" + "github.com/hashicorp/go-multierror" "go.mongodb.org/mongo-driver/bson" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) -type updateJSON = func(map[string]interface{}) +type updateJSON = func(map[string]any) error -func ConvertStringValueToInt64(json map[string]interface{}, path string) { - pos := strings.Index(path, ".") - if pos == -1 { - valueI, ok := json[path] - if !ok { - return +var ErrPathNotFound = errors.New("path not found") + +// ConvertStringValueToInt64 converts string values to int64 in a JSON map based on provided paths. +// It iterates over the specified paths in the JSON map and converts the string values found at those paths to int64 values. +// If permitMissingPaths is set to true, missing paths in the JSON map will be ignored and the modified JSON map will be returned. +// If permitMissingPaths is set to false, an error will be returned if any of the specified paths are not found in the JSON map. +// The function returns the updated JSON map with the converted int64 values. +// If an error occurs during the conversion, the partially modified JSON map is returned along with the error. +func ConvertStringValueToInt64(jsonMap any, permitMissingPaths bool, paths ...string) (any, error) { + for _, path := range paths { + newMap, err := convertPath(jsonMap, permitMissingPaths, path) + if err != nil { + return jsonMap, err + } + jsonMap = newMap + } + return jsonMap, nil +} + +func handleSlice(slice []any, permitMissingPaths bool, remainingParts []string) ([]any, error) { + var ( + parents []any + errs *multierror.Error + ) + + for _, item := range slice { + p, err := findParents(item, permitMissingPaths, remainingParts) + if err != nil { + errs = multierror.Append(errs, err) + } else if p != nil { + parents = append(parents, p...) } - valueStr, ok := valueI.(string) - if !ok { - return + } + + if len(parents) == 0 { + return nil, errs.ErrorOrNil() + } + + return parents, errs.ErrorOrNil() +} + +func iterateOverMap(curr map[string]any, permitMissingPaths bool, part string) (any, error) { + var current any + if value, exists := curr[part]; exists { + current = value + } else if permitMissingPaths { + return nil, nil + } else { + return nil, fmt.Errorf("path segment %s: %w", part, ErrPathNotFound) + } + return current, nil +} + +func iterateOverSlice(curr []any, permitMissingPaths bool, part string) (any, error) { + index, err := strconv.Atoi(part) + if err != nil { + return nil, fmt.Errorf("invalid array index %s", part) + } + if index < 0 || index >= len(curr) { + if permitMissingPaths { + return nil, nil } - value, err := strconv.ParseInt(valueStr, 10, 64) + return nil, fmt.Errorf("index out of range %d: %w", index, ErrPathNotFound) + } + return curr[index], nil +} + +func nextCurrent(current any, permitMissingPaths bool, parts []string, idx int) ([]any, any, error) { + part := parts[idx] + if part == "" { + return nil, current, nil + } + var err error + switch curr := current.(type) { + case map[string]any: + current, err = iterateOverMap(curr, permitMissingPaths, part) + case []any: + if part == "*" { + val, err2 := handleSlice(curr, permitMissingPaths, parts[idx+1:]) + return val, nil, err2 + } + current, err = iterateOverSlice(curr, permitMissingPaths, part) + default: + return nil, nil, fmt.Errorf("unsupported type %T at path segment %s", current, part) + } + if err != nil { + return nil, nil, err + } + if current == nil { + return nil, nil, nil + } + return nil, current, nil +} + +func findParents(current any, permitMissingPaths bool, parts []string) ([]any, error) { + for idx := range parts { + var result []any + var err error + result, next, err := nextCurrent(current, permitMissingPaths, parts, idx) if err != nil { - return + return nil, err } - json[path] = value - return + if result != nil { + return result, nil + } + if next == nil { + return nil, nil + } + current = next } + return []any{current}, nil +} - elemPath := path[:pos] - elem, ok := json[elemPath] +var splitPathRE = regexp.MustCompile(`\.\[|\]\.|\.|\[|\]`) + +func splitPath(path string) []string { + parts := splitPathRE.Split(path, -1) + var cleanParts []string + for _, part := range parts { + if part != "" { + cleanParts = append(cleanParts, part) + } + } + return cleanParts +} + +func setMap(data any, permitMissingPaths bool, path string, parent map[string]any, lastPart string) (out any, err error) { + value, exists := parent[lastPart] + if !exists { + if permitMissingPaths { + return data, nil + } + return data, fmt.Errorf("path %s: %w", path, ErrPathNotFound) + } + strVal, ok := value.(string) if !ok { - return - } - elemArray, ok := elem.([]interface{}) - if ok { - for i, elem := range elemArray { - elemMap, ok2 := elem.(map[string]interface{}) - if !ok2 { - continue + return data, fmt.Errorf("expected string at path %s, but found %T", path, value) + } + intVal, err := setDirectValue(strVal, path) + if err != nil { + return data, err + } + parent[lastPart] = intVal + + return data, nil +} + +func setSliceValue(data any, permitMissingPaths bool, path string, parent []any, index int) (out any, err error) { + if index < 0 || index >= len(parent) { + if permitMissingPaths { + return data, nil + } + return data, fmt.Errorf("index out of range %d", index) + } + if value, ok := parent[index].(string); ok { + intVal, err := setDirectValue(value, path) + if err != nil { + return data, err + } + parent[index] = intVal + } else { + return data, fmt.Errorf("expected string at path %s, but found %T", path, parent[index]) + } + return data, nil +} + +func setSlice(data any, permitMissingPaths bool, path string, parent []any, lastPart string) (out any, err error) { + if lastPart == "*" { + out = data + for i := range parent { + out, err = setSliceValue(out, permitMissingPaths, path, parent, i) + if err != nil { + return out, err } - ConvertStringValueToInt64(elemMap, path[pos+1:]) - elemArray[i] = elemMap } - json[elemPath] = elemArray - return + return out, err } - elemMap, ok := elem.(map[string]interface{}) - if !ok { - return + index, err := strconv.Atoi(lastPart) + if err != nil { + return data, fmt.Errorf("invalid array index %s", lastPart) + } + return setSliceValue(data, permitMissingPaths, path, parent, index) +} + +func setDirectValue(data string, path string) (out int64, err error) { + intVal, err := strconv.ParseInt(data, 10, 64) + if err != nil { + return -1, fmt.Errorf("error converting string to int64 at path %s: %w", path, err) } - ConvertStringValueToInt64(elemMap, path[pos+1:]) - json[elemPath] = elemMap + return intVal, nil +} + +func processPath(data any, permitMissingPaths bool, path string) (parentsRaw []any, parts []string, lastPart string, err error) { + if path == "." { + return []any{data}, nil, "", nil + } + parts = splitPath(path) + if len(parts) == 0 { + return nil, nil, "", errors.New("empty path") + } + + lastPart = parts[len(parts)-1] + parentsRaw, err = findParents(data, permitMissingPaths, parts[:len(parts)-1]) + if err != nil { + return nil, nil, "", fmt.Errorf("error finding parent for path %s: %w", path, err) + } + return parentsRaw, parts, lastPart, nil +} + +func convertPath(data any, permitMissingPaths bool, path string) (out any, err error) { + parentsRaw, parts, lastPart, err := processPath(data, permitMissingPaths, path) + if err != nil { + return data, err + } + + out = data + var errs *multierror.Error + for _, parentRaw := range parentsRaw { + switch parent := parentRaw.(type) { + case map[string]any: + out, err = setMap(out, permitMissingPaths, path, parent, lastPart) + if err != nil { + errs = multierror.Append(errs, err) + } + case []any: + out, err = setSlice(out, permitMissingPaths, path, parent, lastPart) + if err != nil { + errs = multierror.Append(errs, err) + } + case string: + out, err = setDirectValue(parent, path) + if err != nil { + errs = multierror.Append(errs, err) + } + default: + return data, fmt.Errorf("unsupported type %T at parent path %s", parent, strings.Join(parts[:len(parts)-1], ".")) + } + } + return out, errs.ErrorOrNil() } func UnmarshalProtoBSON(data []byte, m proto.Message, update updateJSON) error { - var obj map[string]interface{} + var obj map[string]any if err := bson.Unmarshal(data, &obj); err != nil { return err } if update != nil { - update(obj) + if err := update(obj); err != nil { + return err + } } jsonData, err := json.Marshal(obj) if err != nil { @@ -77,13 +279,15 @@ func MarshalProtoBSON(m proto.Message, update updateJSON) ([]byte, error) { if err != nil { return nil, err } - var obj map[string]interface{} + var obj map[string]any err = json.Unmarshal(data, &obj) if err != nil { return nil, err } if update != nil { - update(obj) + if err := update(obj); err != nil { + return nil, err + } } return bson.Marshal(obj) } diff --git a/pkg/mongodb/marshal_test.go b/pkg/mongodb/marshal_test.go new file mode 100644 index 000000000..edeb1ad87 --- /dev/null +++ b/pkg/mongodb/marshal_test.go @@ -0,0 +1,223 @@ +package mongodb_test + +import ( + "testing" + + "github.com/plgd-dev/hub/v2/pkg/mongodb" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConvertStringValueToInt64(t *testing.T) { + type args struct { + data interface{} + paths []string + permitMissingPaths bool + } + + tests := []struct { + name string + args args + want interface{} + wantErr bool + ignoreErr bool + }{ + { + name: "emptyPath", + args: args{ + data: map[string]interface{}{}, + paths: []string{""}, + }, + wantErr: true, + }, + { + name: "invalidPath", + args: args{ + data: map[string]interface{}{}, + paths: []string{"foo"}, + }, + wantErr: true, + }, + { + name: "directValue", + args: args{ + data: "123", + paths: []string{"."}, + }, + want: int64(123), + }, + { + name: "arrayValue", + args: args{ + data: []interface{}{ + "123", + "456", + "789", + }, + paths: []string{".[0]", ".[2]"}, + }, + want: []interface{}{int64(123), "456", int64(789)}, + }, + { + name: "mapValue", + args: args{ + data: map[string]interface{}{ + "foo": "123", + }, + paths: []string{".foo"}, + }, + want: map[string]interface{}{ + "foo": int64(123), + }, + }, + { + name: "mapArrayValue", + args: args{ + data: map[string]interface{}{ + "foo": []interface{}{ + "123", + "456", + "789", + }, + }, + paths: []string{".foo[0]", ".foo[2]"}, + }, + want: map[string]interface{}{ + "foo": []interface{}{int64(123), "456", int64(789)}, + }, + }, + { + name: "nestedMapValue", + args: args{ + data: map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": "123", + }, + }, + paths: []string{".foo.bar"}, + }, + want: map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": int64(123), + }, + }, + }, + { + name: "nestedArrayMapValue", + args: args{ + data: map[string]interface{}{ + "foo": []interface{}{ + map[string]interface{}{ + "bar": "123", + }, + map[string]interface{}{ + "bar": "456", + }, + }, + }, + paths: []string{".foo[0].bar", ".foo[1].bar"}, + }, + want: map[string]interface{}{ + "foo": []interface{}{ + map[string]interface{}{ + "bar": int64(123), + }, + map[string]interface{}{ + "bar": int64(456), + }, + }, + }, + }, + { + name: "nestedArrayMapAllValues", + args: args{ + data: map[string]interface{}{ + "foo": []interface{}{ + map[string]interface{}{ + "bar": "123", + }, + map[string]interface{}{ + "bar": "456", + }, + }, + }, + paths: []string{".foo[*].bar"}, + }, + want: map[string]interface{}{ + "foo": []interface{}{ + map[string]interface{}{ + "bar": int64(123), + }, + map[string]interface{}{ + "bar": int64(456), + }, + }, + }, + }, + { + name: "nestedArrayMapWithMissingPathsAllValues", + args: args{ + data: map[string]interface{}{ + "foo": []interface{}{ + map[string]interface{}{ + "bar": "123", + }, + map[string]interface{}{ + "efg": "456", + }, + map[string]interface{}{ + "bar": "789", + }, + }, + }, + paths: []string{".foo[*].bar"}, + permitMissingPaths: true, + }, + want: map[string]interface{}{ + "foo": []interface{}{ + map[string]interface{}{ + "bar": int64(123), + }, + map[string]interface{}{ + "efg": "456", + }, + map[string]interface{}{ + "bar": int64(789), + }, + }, + }, + }, + { + name: "mapArrayAllValues", + args: args{ + data: map[string]interface{}{ + "foo": []interface{}{ + "123", + "456", + "789", + }, + }, + paths: []string{".foo[*]"}, + }, + want: map[string]interface{}{ + "foo": []interface{}{int64(123), int64(456), int64(789)}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := mongodb.ConvertStringValueToInt64(tt.args.data, tt.args.permitMissingPaths, tt.args.paths...) + if tt.wantErr { + require.Error(t, err) + if !tt.ignoreErr { + return + } + } + if !tt.ignoreErr { + require.NoError(t, err) + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/net/grpc/auth.go b/pkg/net/grpc/auth.go index 6b2bea660..4bf60c16b 100644 --- a/pkg/net/grpc/auth.go +++ b/pkg/net/grpc/auth.go @@ -43,7 +43,7 @@ func (f AuthInterceptors) Stream() grpc.StreamServerInterceptor { type ( ClaimsFunc = func(ctx context.Context, method string) jwt.ClaimsValidator Validator interface { - ParseWithClaims(token string, claims jwt.Claims) error + ParseWithClaims(ctx context.Context, token string, claims jwt.Claims) error } ) @@ -53,7 +53,7 @@ func ValidateJWTWithValidator(validator Validator, claims ClaimsFunc) Intercepto if err != nil { return nil, err } - err = validator.ParseWithClaims(token, claims(ctx, method)) + err = validator.ParseWithClaims(ctx, token, claims(ctx, method)) if err != nil { return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err) } diff --git a/pkg/net/http/auth.go b/pkg/net/http/auth.go index 4dc64be5b..e4cd006a7 100644 --- a/pkg/net/http/auth.go +++ b/pkg/net/http/auth.go @@ -2,31 +2,24 @@ package http import ( "context" - "fmt" "net/http" "regexp" "strings" - "github.com/golang-jwt/jwt/v5" "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgHttpJwt "github.com/plgd-dev/hub/v2/pkg/net/http/jwt" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) -type ( - ClaimsFunc = func(ctx context.Context, method, uri string) jwt.ClaimsValidator - OnUnauthorizedAccessFunc = func(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) - Validator interface { - ParseWithClaims(token string, claims jwt.Claims) error - } -) +type OnUnauthorizedAccessFunc = func(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) // NewDefaultAuthorizationRules returns a map of HTTP methods to a slice of AuthArgs. // The AuthArgs contain a URI field that is a regular expression matching the given apiPath // with any path suffix. This function is used to create default authorization rules for // HTTP methods GET, POST, DELETE, and PUT. -func NewDefaultAuthorizationRules(apiPath string) map[string][]AuthArgs { - return map[string][]AuthArgs{ +func NewDefaultAuthorizationRules(apiPath string) map[string][]pkgHttpJwt.AuthArgs { + return map[string][]pkgHttpJwt.AuthArgs{ http.MethodGet: { { URI: regexp.MustCompile(regexp.QuoteMeta(apiPath) + AnyPathSuffixRegex), @@ -52,58 +45,17 @@ func NewDefaultAuthorizationRules(apiPath string) map[string][]AuthArgs { const ( AnyPathSuffixRegex = `\/.*` - - bearerKey = "bearer" -) - -type key int - -const ( - authorizationKey key = 0 ) -func ctxWithToken(ctx context.Context, token string) context.Context { - if !strings.HasPrefix(strings.ToLower(token), bearerKey+" ") { - token = fmt.Sprintf("%s %s", bearerKey, token) - } - return context.WithValue(ctx, authorizationKey, token) -} - -func tokenFromCtx(ctx context.Context) (string, error) { - val := ctx.Value(authorizationKey) - if bearer, ok := val.(string); ok && strings.HasPrefix(strings.ToLower(bearer), bearerKey+" ") { - token := bearer[7:] - if token == "" { - return "", status.Errorf(codes.Unauthenticated, "invalid token") - } - return token, nil - } - return "", status.Errorf(codes.Unauthenticated, "token not found") -} - -func ParseToken(auth string) (string, error) { +func GetToken(auth string) (string, error) { if strings.HasPrefix(strings.ToLower(auth), "bearer ") { return auth[7:], nil } return "", status.Errorf(codes.Unauthenticated, "cannot parse bearer: prefix 'Bearer ' not found") } -func validateJWTWithValidator(validator Validator, claims ClaimsFunc) Interceptor { - return func(ctx context.Context, method, uri string) (context.Context, error) { - token, err := tokenFromCtx(ctx) - if err != nil { - return nil, err - } - err = validator.ParseWithClaims(token, claims(ctx, method, uri)) - if err != nil { - return nil, fmt.Errorf("invalid token: %w", err) - } - return ctx, nil - } -} - // CreateAuthMiddleware creates middleware for authorization -func CreateAuthMiddleware(authInterceptor Interceptor, onUnauthorizedAccessFunc OnUnauthorizedAccessFunc) func(next http.Handler) http.Handler { +func CreateAuthMiddleware(authInterceptor pkgHttpJwt.Interceptor, onUnauthorizedAccessFunc OnUnauthorizedAccessFunc) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { @@ -111,14 +63,14 @@ func CreateAuthMiddleware(authInterceptor Interceptor, onUnauthorizedAccessFunc next.ServeHTTP(w, r) default: token := r.Header.Get("Authorization") - ctx := ctxWithToken(r.Context(), token) + ctx := pkgHttpJwt.CtxWithToken(r.Context(), token) _, err := authInterceptor(ctx, r.Method, r.RequestURI) if err != nil { onUnauthorizedAccessFunc(ctx, w, r, err) return } - if rawToken, err := ParseToken(token); err == nil { + if rawToken, err := GetToken(token); err == nil { r = r.WithContext(grpc.CtxWithToken(r.Context(), rawToken)) } next.ServeHTTP(w, r) diff --git a/pkg/net/http/header.go b/pkg/net/http/header.go index a882347c2..f278f851c 100644 --- a/pkg/net/http/header.go +++ b/pkg/net/http/header.go @@ -3,10 +3,14 @@ package http const ( ApplicationProtoJsonContentType = "application/protojson" - CorrelationIDHeaderKey = "Correlation-Id" + AcceptHeaderKey = "Accept" + AuthorizationHeaderKey = "Authorization" + ConnectionHeaderKey = "Connection" ContentLengthHeaderKey = "Content-Length" ContentTypeHeaderKey = "Content-Type" ContentTypeOptionsHeaderKey = "X-Content-Type-Options" - AcceptHeaderKey = "Accept" + CorrelationIDHeaderKey = "Correlation-Id" ETagHeaderKey = "ETag" + + AuthorizationBearerPrefix = "Bearer " ) diff --git a/pkg/net/http/interceptor.go b/pkg/net/http/jwt/claims.go similarity index 50% rename from pkg/net/http/interceptor.go rename to pkg/net/http/jwt/claims.go index 08f6e425c..b35a59db1 100644 --- a/pkg/net/http/interceptor.go +++ b/pkg/net/http/jwt/claims.go @@ -1,41 +1,13 @@ -package http +package jwt import ( "context" "fmt" - "regexp" - "strings" "github.com/golang-jwt/jwt/v5" pkgJwt "github.com/plgd-dev/hub/v2/pkg/security/jwt" ) -type Interceptor = func(ctx context.Context, method, uri string) (context.Context, error) - -type AuthArgs struct { - URI *regexp.Regexp - Scopes []*regexp.Regexp -} - -// RequestMatcher allows request without token validation. -type RequestMatcher struct { - Method string - URI *regexp.Regexp -} - -// NewInterceptor authorizes HTTP request with validator. -func NewInterceptorWithValidator(validator Validator, auths map[string][]AuthArgs, whiteList ...RequestMatcher) Interceptor { - validateJWT := validateJWTWithValidator(validator, MakeClaimsFunc(auths)) - return func(ctx context.Context, method, uri string) (context.Context, error) { - for _, wa := range whiteList { - if strings.EqualFold(method, wa.Method) && wa.URI.MatchString(uri) { - return ctx, nil - } - } - return validateJWT(ctx, method, uri) - } -} - type DeniedClaimsError struct { jwt.MapClaims Err error diff --git a/pkg/net/http/jwt/interceptor.go b/pkg/net/http/jwt/interceptor.go new file mode 100644 index 000000000..1ca2a5a94 --- /dev/null +++ b/pkg/net/http/jwt/interceptor.go @@ -0,0 +1,56 @@ +package jwt + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/golang-jwt/jwt/v5" +) + +type ( + ClaimsFunc = func(ctx context.Context, method, uri string) jwt.ClaimsValidator + Validator interface { + ParseWithClaims(ctx context.Context, token string, claims jwt.Claims) error + } + Interceptor = func(ctx context.Context, method, uri string) (context.Context, error) +) + +type AuthArgs struct { + URI *regexp.Regexp + Scopes []*regexp.Regexp +} + +// RequestMatcher allows request without token validation. +type RequestMatcher struct { + Method string + URI *regexp.Regexp +} + +func validateJWTWithValidator(validator Validator, claims ClaimsFunc) Interceptor { + return func(ctx context.Context, method, uri string) (context.Context, error) { + token, err := tokenFromCtx(ctx) + if err != nil { + return nil, err + } + err = validator.ParseWithClaims(ctx, token, claims(ctx, method, uri)) + if err != nil { + return nil, fmt.Errorf("invalid token: %w", err) + } + return ctx, nil + } +} + +// NewInterceptor authorizes HTTP request with validator. +func NewInterceptorWithValidator(validator Validator, auths map[string][]AuthArgs, whiteList ...RequestMatcher) Interceptor { + validateJWT := validateJWTWithValidator(validator, MakeClaimsFunc(auths)) + return func(ctx context.Context, method, uri string) (context.Context, error) { + for _, wa := range whiteList { + if strings.EqualFold(method, wa.Method) && wa.URI.MatchString(uri) { + return ctx, nil + } + } + return validateJWT(ctx, method, uri) + } +} diff --git a/pkg/net/http/jwt/token.go b/pkg/net/http/jwt/token.go new file mode 100644 index 000000000..b3fc8d2f4 --- /dev/null +++ b/pkg/net/http/jwt/token.go @@ -0,0 +1,50 @@ +package jwt + +import ( + "context" + "fmt" + "strings" + + pkgJwt "github.com/plgd-dev/hub/v2/pkg/security/jwt" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type key int + +const ( + authorizationKey key = 0 + + bearerKey = "bearer" +) + +func CtxWithToken(ctx context.Context, token string) context.Context { + if !strings.HasPrefix(strings.ToLower(token), bearerKey+" ") { + token = fmt.Sprintf("%s %s", bearerKey, token) + } + return context.WithValue(ctx, authorizationKey, token) +} + +func tokenFromCtx(ctx context.Context) (string, error) { + val := ctx.Value(authorizationKey) + if bearer, ok := val.(string); ok && strings.HasPrefix(strings.ToLower(bearer), bearerKey+" ") { + token := bearer[7:] + if token == "" { + return "", status.Errorf(codes.Unauthenticated, "invalid token") + } + return token, nil + } + return "", status.Errorf(codes.Unauthenticated, "token not found") +} + +func SubjectFromToken(token string) (string, bool) { + claims, err := pkgJwt.ParseToken(token) + if err != nil { + return "", false + } + subject, err := claims.GetSubject() + if err != nil { + return "", false + } + return subject, true +} diff --git a/pkg/net/http/auth_test.go b/pkg/net/http/jwt/token_test.go similarity index 62% rename from pkg/net/http/auth_test.go rename to pkg/net/http/jwt/token_test.go index 7ab8f571d..ac72b6027 100644 --- a/pkg/net/http/auth_test.go +++ b/pkg/net/http/jwt/token_test.go @@ -1,4 +1,4 @@ -package http +package jwt import ( "context" @@ -9,15 +9,15 @@ import ( func TestCtxWithToken(t *testing.T) { ctx := context.Background() - token, err := tokenFromCtx(ctxWithToken(ctx, "a")) + token, err := tokenFromCtx(CtxWithToken(ctx, "a")) require.NoError(t, err) require.Equal(t, "a", token) - token, err = tokenFromCtx(ctxWithToken(ctx, bearerKey+" b")) + token, err = tokenFromCtx(CtxWithToken(ctx, bearerKey+" b")) require.NoError(t, err) require.Equal(t, "b", token) - token, err = tokenFromCtx(ctxWithToken(ctx, "Bearer c")) + token, err = tokenFromCtx(CtxWithToken(ctx, "Bearer c")) require.NoError(t, err) require.Equal(t, "c", token) } diff --git a/pkg/net/http/loggingMiddleware.go b/pkg/net/http/loggingMiddleware.go index e49731803..b16455fa3 100644 --- a/pkg/net/http/loggingMiddleware.go +++ b/pkg/net/http/loggingMiddleware.go @@ -11,7 +11,7 @@ import ( "github.com/plgd-dev/go-coap/v3/message" "github.com/plgd-dev/hub/v2/pkg/log" - pkgJwt "github.com/plgd-dev/hub/v2/pkg/security/jwt" + pkgHttpJwt "github.com/plgd-dev/hub/v2/pkg/net/http/jwt" "go.opentelemetry.io/otel/trace" rpcStatus "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc/status" @@ -229,14 +229,11 @@ func createLogRequest(r *http.Request) *request { } token := strings.SplitN(bearer, " ", 2) if len(token) == 2 && strings.ToLower(token[0]) == "bearer" { - claims, err := pkgJwt.ParseToken(token[1]) - if err != nil { - return &req - } - subject, err := claims.GetSubject() - if err != nil { + subject, ok := pkgHttpJwt.SubjectFromToken(token[1]) + if !ok { return &req } + req.JWT = &jwtMember{ Sub: subject, } diff --git a/pkg/net/http/pb/protojson.go b/pkg/net/http/pb/protojson.go new file mode 100644 index 000000000..af3399580 --- /dev/null +++ b/pkg/net/http/pb/protojson.go @@ -0,0 +1,82 @@ +package pb + +import ( + "encoding/json" + "io" + "net/http" + + jsoniter "github.com/json-iterator/go" + "github.com/plgd-dev/hub/v2/pkg/log" + "google.golang.org/genproto/googleapis/rpc/status" + grpcStatus "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/reflect/protoreflect" +) + +func UnmarshalError(data []byte) error { + var s status.Status + err := protojson.Unmarshal(data, &s) + if err != nil { + return err + } + return grpcStatus.ErrorProto(&s) +} + +type Decoder struct { + logger log.Logger +} + +func NewDecoder(logger log.Logger) *Decoder { + return &Decoder{ + logger: logger, + } +} + +func Unmarshal(code int, input io.Reader, v protoreflect.ProtoMessage) error { + d := NewDecoder(log.Get()) + return d.Unmarshal(code, input, v) +} + +func (d *Decoder) Unmarshal(code int, input io.Reader, v protoreflect.ProtoMessage) error { + var data json.RawMessage + err := json.NewDecoder(input).Decode(&data) + if err != nil { + return err + } + d.logger.Debugf("data: %s\n", data) + + if code != http.StatusOK { + return UnmarshalError(data) + } + + var item struct { + Result json.RawMessage `json:"result"` + Error json.RawMessage `json:"error"` + } + + err = jsoniter.Unmarshal(data, &item) + if err != nil { + return err + } + if len(item.Result) == 0 && len(item.Error) == 0 { + u := protojson.UnmarshalOptions{ + DiscardUnknown: true, + } + err = u.Unmarshal(data, v) + if err != nil { + return err + } + return nil + } + if len(item.Error) > 0 { + return UnmarshalError(item.Error) + } + u := protojson.UnmarshalOptions{ + DiscardUnknown: true, + } + err = u.Unmarshal(item.Result, v) + if err != nil { + return err + } + return nil +} diff --git a/pkg/net/http/pb/protojson_test.go b/pkg/net/http/pb/protojson_test.go new file mode 100644 index 000000000..2a06c6cd3 --- /dev/null +++ b/pkg/net/http/pb/protojson_test.go @@ -0,0 +1,102 @@ +package pb_test + +import ( + "bytes" + "net/http" + "testing" + + "github.com/plgd-dev/hub/v2/pkg/net/http/pb" + "github.com/plgd-dev/hub/v2/test" + "github.com/stretchr/testify/require" + "google.golang.org/genproto/googleapis/rpc/status" + grpcStatus "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/structpb" +) + +func TestUnmarshalError(t *testing.T) { + s := &status.Status{ + Code: http.StatusInternalServerError, + Message: "test error", + } + data, err := protojson.Marshal(s) + require.NoError(t, err) + + err = pb.UnmarshalError(data) + require.Error(t, err) + + st, ok := grpcStatus.FromError(err) + require.True(t, ok) + require.Equal(t, s.GetCode(), int32(st.Code())) + require.Equal(t, s.GetMessage(), st.Message()) +} + +func TestUnmarshal(t *testing.T) { + tests := []struct { + name string + code int + input []byte + wantGrpcError error + wantErr bool + want protoreflect.ProtoMessage + }{ + { + name: "Unmarshal success", + code: http.StatusOK, + input: func() []byte { + data, err := protojson.Marshal(structpb.NewStringValue("test")) + require.NoError(t, err) + return []byte(`{"result":` + string(data) + `}`) + }(), + want: structpb.NewStringValue("test"), + }, + { + name: "Unmarshal error status", + code: http.StatusInternalServerError, + input: []byte(`{"code": 500, "message": "test error"}`), + wantGrpcError: grpcStatus.ErrorProto(&status.Status{ + Code: http.StatusInternalServerError, + Message: "test error", + }), + }, + { + name: "Unmarshal error status (2)", + code: http.StatusOK, + input: []byte(`{"error": {"code": 500, "message": "test error"}}`), + wantGrpcError: grpcStatus.ErrorProto(&status.Status{ + Code: http.StatusInternalServerError, + Message: "test error", + }), + }, + { + name: "Invalid JSON", + code: http.StatusOK, + input: []byte(`invalid json`), + wantErr: true, + }, + { + name: "Empty result and error fields", + code: http.StatusOK, + input: []byte(`{}`), + want: &structpb.Struct{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var v structpb.Value + err := pb.Unmarshal(tt.code, bytes.NewReader(tt.input), &v) + if tt.wantGrpcError != nil { + require.ErrorIs(t, err, tt.wantGrpcError) + return + } + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + test.CheckProtobufs(t, tt.want, &v, test.RequireToCheckFunc(require.Equal)) + }) + } +} diff --git a/pkg/net/http/service/config.go b/pkg/net/http/service/config.go index cdde59691..5ee7214b5 100644 --- a/pkg/net/http/service/config.go +++ b/pkg/net/http/service/config.go @@ -7,7 +7,7 @@ import ( "github.com/plgd-dev/hub/v2/pkg/config" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" - kitNetHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpJwt "github.com/plgd-dev/hub/v2/pkg/net/http/jwt" "github.com/plgd-dev/hub/v2/pkg/net/http/server" "github.com/plgd-dev/hub/v2/pkg/net/listener" "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" @@ -21,8 +21,8 @@ type Config struct { ServiceName string FileWatcher *fsnotify.Watcher Logger log.Logger - WhiteEndpointList []kitNetHttp.RequestMatcher - AuthRules map[string][]kitNetHttp.AuthArgs + WhiteEndpointList []pkgHttpJwt.RequestMatcher + AuthRules map[string][]pkgHttpJwt.AuthArgs TraceProvider trace.TracerProvider Validator *validator.Validator QueryCaseInsensitive map[string]string diff --git a/pkg/net/http/service/service.go b/pkg/net/http/service/service.go index d23257265..a2aef07c3 100644 --- a/pkg/net/http/service/service.go +++ b/pkg/net/http/service/service.go @@ -9,7 +9,8 @@ import ( "github.com/gorilla/mux" "github.com/plgd-dev/hub/v2/http-gateway/serverMux" - kitNetHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpJwt "github.com/plgd-dev/hub/v2/pkg/net/http/jwt" "github.com/plgd-dev/hub/v2/pkg/net/listener" ) @@ -30,12 +31,12 @@ func New(config Config) (*Service, error) { } router := mux.NewRouter() - auth := kitNetHttp.NewInterceptorWithValidator(config.Validator, config.AuthRules, config.WhiteEndpointList...) - r0 := serverMux.NewRouter(config.QueryCaseInsensitive, auth, kitNetHttp.WithLogger(config.Logger)) + auth := pkgHttpJwt.NewInterceptorWithValidator(config.Validator, config.AuthRules, config.WhiteEndpointList...) + r0 := serverMux.NewRouter(config.QueryCaseInsensitive, auth, pkgHttp.WithLogger(config.Logger)) r0.PathPrefix("/").Handler(router) httpServer := http.Server{ - Handler: kitNetHttp.OpenTelemetryNewHandler(r0, config.ServiceName, config.TraceProvider), + Handler: pkgHttp.OpenTelemetryNewHandler(r0, config.ServiceName, config.TraceProvider), ReadTimeout: config.HTTPServer.ReadTimeout, ReadHeaderTimeout: config.HTTPServer.ReadHeaderTimeout, WriteTimeout: config.HTTPServer.WriteTimeout, diff --git a/pkg/net/http/toUrlString.go b/pkg/net/http/toUrlString.go deleted file mode 100644 index c35145ba2..000000000 --- a/pkg/net/http/toUrlString.go +++ /dev/null @@ -1,13 +0,0 @@ -package http - -import "net/url" - -// ToURLString convert scheme, host, path to escaped url. -func ToURLString(scheme string, host string, path string) string { - url := url.URL{ - Scheme: scheme, - Host: host, - Path: path, - } - return url.String() -} diff --git a/pkg/net/http/toUrlString_test.go b/pkg/net/http/toUrlString_test.go deleted file mode 100644 index ee7f74e9f..000000000 --- a/pkg/net/http/toUrlString_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package http - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestToURLString(t *testing.T) { - type args struct { - scheme string - host string - path string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "a://b/c", - args: args{ - scheme: "a", - host: "b", - path: "c", - }, - want: "a://b/c", - }, - { - name: "a://b/%", - args: args{ - scheme: "a", - host: "b", - path: "%", - }, - want: "a://b/%25", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := ToURLString(tt.args.scheme, tt.args.host, tt.args.path) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/pkg/net/http/canonicalHref.go b/pkg/net/http/uri/canonicalHref.go similarity index 51% rename from pkg/net/http/canonicalHref.go rename to pkg/net/http/uri/canonicalHref.go index c32bc8e62..ea1cf849b 100644 --- a/pkg/net/http/canonicalHref.go +++ b/pkg/net/http/uri/canonicalHref.go @@ -1,4 +1,4 @@ -package http +package uri import ( "regexp" @@ -7,10 +7,22 @@ import ( // CanonicalHref always lead by "/" func CanonicalHref(href string) string { + p := CanonicalURI(href) + p = strings.TrimLeft(p, "/") + return "/" + p +} + +func CanonicalURI(uri string) string { + var schema string + href := uri + components := strings.SplitN(uri, "://", 2) + if len(components) > 1 { + schema = components[0] + "://" + href = components[1] + } + backslash := regexp.MustCompile(`\/+`) p := backslash.ReplaceAllString(href, "/") - p = strings.TrimLeft(p, "/") p = strings.TrimRight(p, "/") - - return "/" + p + return schema + p } diff --git a/pkg/net/http/canonicalHref_test.go b/pkg/net/http/uri/canonicalHref_test.go similarity index 78% rename from pkg/net/http/canonicalHref_test.go rename to pkg/net/http/uri/canonicalHref_test.go index a58bcdea7..ebcb06a77 100644 --- a/pkg/net/http/canonicalHref_test.go +++ b/pkg/net/http/uri/canonicalHref_test.go @@ -1,6 +1,10 @@ -package http +package uri_test -import "testing" +import ( + "testing" + + "github.com/plgd-dev/hub/v2/pkg/net/http/uri" +) func TestCanonicalHref(t *testing.T) { type args struct { @@ -35,7 +39,7 @@ func TestCanonicalHref(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := CanonicalHref(tt.args.href); got != tt.want { + if got := uri.CanonicalHref(tt.args.href); got != tt.want { t.Errorf("CanonicalHref() = %v, want %v", got, tt.want) } }) diff --git a/pkg/security/jwt/claims.go b/pkg/security/jwt/claims.go index d46071f2c..92fdb5201 100644 --- a/pkg/security/jwt/claims.go +++ b/pkg/security/jwt/claims.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/golang-jwt/jwt/v5" + pkgHttpUri "github.com/plgd-dev/hub/v2/pkg/net/http/uri" pkgStrings "github.com/plgd-dev/hub/v2/pkg/strings" ) @@ -22,7 +23,7 @@ const ( ClaimID = "jti" ClaimEmail = "email" ClaimClientID = "client_id" - ClaimName = "n" + ClaimName = "name" ) var ErrOwnerClaimInvalid = errors.New("owner claim is invalid") @@ -158,3 +159,42 @@ func ParseToken(token string) (Claims, error) { } return claims, nil } + +func getIssuer(token *jwt.Token) (string, error) { + if token == nil { + return "", ErrMissingToken + } + if token.Claims == nil { + return "", ErrMissingClaims + } + + switch claims := token.Claims.(type) { + case interface{ GetIssuer() (string, error) }: + issuer, err := claims.GetIssuer() + if err != nil { + return "", ErrMissingIssuer + } + return pkgHttpUri.CanonicalURI(issuer), nil + default: + return "", fmt.Errorf("unsupported type %T", token.Claims) + } +} + +func getID(claims jwt.Claims) (string, error) { + switch c := claims.(type) { + case interface{ GetID() (string, error) }: + id, err := c.GetID() + if err != nil { + return "", ErrMissingID + } + return id, nil + case jwt.MapClaims: + id, ok := c[ClaimID].(string) + if !ok { + return "", ErrMissingID + } + return id, nil + default: + return "", fmt.Errorf("unsupported type %T", claims) + } +} diff --git a/pkg/security/jwt/multiJwk.go b/pkg/security/jwt/multiJwk.go index ba2f88311..ba0780509 100644 --- a/pkg/security/jwt/multiJwk.go +++ b/pkg/security/jwt/multiJwk.go @@ -13,6 +13,7 @@ import ( var ( ErrMissingClaims = errors.New("missing claims") ErrMissingIssuer = errors.New("missing issuer") + ErrMissingID = errors.New("missing jti") ) type MultiKeyCache struct { @@ -33,32 +34,6 @@ func (c *MultiKeyCache) GetOrFetchKey(token *jwt.Token) (interface{}, error) { return c.GetOrFetchKeyWithContext(context.Background(), token) } -func getIssuer(token *jwt.Token) (string, error) { - if token == nil { - return "", ErrMissingToken - } - if token.Claims == nil { - return "", ErrMissingClaims - } - - switch claims := token.Claims.(type) { - case jwt.MapClaims: - issuer, ok := claims["iss"].(string) - if !ok { - return "", ErrMissingIssuer - } - return strings.TrimSuffix(issuer, "/"), nil - case interface{ GetIssuer() (string, error) }: - issuer, err := claims.GetIssuer() - if err != nil { - return "", ErrMissingIssuer - } - return strings.TrimSuffix(issuer, "/"), nil - default: - return "", fmt.Errorf("unsupported type %T", token.Claims) - } -} - func checkForError(token *jwt.Token) error { if claims, ok := token.Claims.(interface { Error() string diff --git a/pkg/security/jwt/scopeClaims.go b/pkg/security/jwt/scopeClaims.go index c149c0cd5..e97f27b3a 100644 --- a/pkg/security/jwt/scopeClaims.go +++ b/pkg/security/jwt/scopeClaims.go @@ -52,6 +52,10 @@ func (c *ScopeClaims) GetAudience() (jwt.ClaimStrings, error) { return Claims(*c).GetAudience() } +func (c *ScopeClaims) GetID() (string, error) { + return Claims(*c).GetID() +} + func (c *ScopeClaims) Validate() error { v := Claims(*c) rs, ok := v[PlgdRequiredScope] diff --git a/pkg/security/jwt/tokenCache.go b/pkg/security/jwt/tokenCache.go new file mode 100644 index 000000000..032061386 --- /dev/null +++ b/pkg/security/jwt/tokenCache.go @@ -0,0 +1,291 @@ +package jwt + +import ( + "context" + "fmt" + "net/http" + "net/url" + "sync" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/m2m-oauth-server/pb" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" + "github.com/plgd-dev/hub/v2/pkg/sync/task/future" + "go.uber.org/atomic" +) + +type Client struct { + *http.Client + tokenEndpoint string +} + +func NewClient(client *http.Client, tokenEndpoint string) *Client { + return &Client{ + Client: client, + tokenEndpoint: tokenEndpoint, + } +} + +type tokenRecord struct { + blacklisted bool + validUntil atomic.Time + onExpire func(uuid.UUID) +} + +func newTokenRecord(blacklisted bool, validUntil time.Time, onExpire func(uuid.UUID)) *tokenRecord { + t := tokenRecord{ + blacklisted: blacklisted, + onExpire: onExpire, + } + t.validUntil.Store(validUntil) + return &t +} + +func (t *tokenRecord) IsExpired(now time.Time) bool { + value := t.validUntil.Load() + if value.IsZero() { + return false + } + return now.After(value) +} + +type tokenOrFuture struct { + tokenOrFuture interface{} +} + +func makeTokenOrFuture(token *tokenRecord, tokenFuture *future.Future) tokenOrFuture { + if token != nil { + return tokenOrFuture{token} + } + return tokenOrFuture{tokenFuture} +} + +func (tf *tokenOrFuture) Get(ctx context.Context) (*tokenRecord, error) { + if tr, ok := tf.tokenOrFuture.(*tokenRecord); ok { + return tr, nil + } + tv, err := tf.tokenOrFuture.(*future.Future).Get(ctx) + if err != nil { + return nil, err + } + return tv.(*tokenRecord), nil +} + +type tokenIssuerCache struct { + client *http.Client + tokenEndpoint string + tokens map[uuid.UUID]tokenOrFuture + mutex sync.Mutex +} + +func newTokenIssuerCache(client *Client) *tokenIssuerCache { + return &tokenIssuerCache{ + client: client.Client, + tokenEndpoint: client.tokenEndpoint, + tokens: make(map[uuid.UUID]tokenOrFuture), + } +} + +func (tc *tokenIssuerCache) getValidTokenRecordOrFuture(tokenID uuid.UUID) (tokenOrFuture, future.SetFunc) { + tc.mutex.Lock() + defer tc.mutex.Unlock() + + tf, ok := tc.tokens[tokenID] + if !ok { + f, set := future.New() + newTf := makeTokenOrFuture(nil, f) + tc.tokens[tokenID] = newTf + return newTf, set + } + + if tr, ok := tf.tokenOrFuture.(*tokenRecord); ok && tr.IsExpired(time.Now()) { + if tr.onExpire != nil { + tr.onExpire(tokenID) + } + f, set := future.New() + newTr := makeTokenOrFuture(nil, f) + tc.tokens[tokenID] = newTr + return newTr, set + } + return tf, nil +} + +func (tc *tokenIssuerCache) removeTokenRecord(tokenID uuid.UUID) { + tc.mutex.Lock() + defer tc.mutex.Unlock() + delete(tc.tokens, tokenID) +} + +func (tc *tokenIssuerCache) removeTokenRecordAndSetErrorOnFuture(tokenUUID uuid.UUID, setTRFuture future.SetFunc, err error) { + tc.removeTokenRecord(tokenUUID) + setTRFuture(nil, err) +} + +func (tc *tokenIssuerCache) setTokenRecord(tokenUUID uuid.UUID, tr *tokenRecord) { + tf := makeTokenOrFuture(tr, nil) + tc.mutex.Lock() + defer tc.mutex.Unlock() + tc.tokens[tokenUUID] = tf +} + +func (tc *tokenIssuerCache) setTokenRecordAndWaitingFuture(tokenUUID uuid.UUID, tr *tokenRecord, setTRFuture future.SetFunc) { + tc.setTokenRecord(tokenUUID, tr) + setTRFuture(tr, nil) +} + +func (tc *tokenIssuerCache) checkExpirations(now time.Time) { + expired := make(map[uuid.UUID]*tokenRecord, 8) + tc.mutex.Lock() + for tokenID, tf := range tc.tokens { + if tr, ok := tf.tokenOrFuture.(*tokenRecord); ok && tr.IsExpired(now) { + if tr.onExpire != nil { + expired[tokenID] = tr + } + delete(tc.tokens, tokenID) + } + } + tc.mutex.Unlock() + for tokenID, tr := range expired { + tr.onExpire(tokenID) + } +} + +func (tc *tokenIssuerCache) verifyTokenByRequest(ctx context.Context, token, tokenID string) (*pb.Token, error) { + uri, err := url.Parse(tc.tokenEndpoint) + if err != nil { + return nil, fmt.Errorf("cannot parse tokenEndpoint %v: %w", tc.tokenEndpoint, err) + } + query := uri.Query() + query.Add("idFilter", tokenID) + query.Add("includeBlacklisted", "true") + uri.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, fmt.Errorf("cannot create request for GET %v: %w", uri.String(), err) + } + + req.Header.Set("Accept", "application/protojson") + req.Header.Set("Authorization", "bearer "+token) + resp, err := tc.client.Do(req) + if err != nil { + return nil, fmt.Errorf("cannot send request for GET %v: %w", tc.tokenEndpoint, err) + } + + defer func() { + _ = resp.Body.Close() + }() + + var gotToken pb.Token + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &gotToken) + if err != nil { + return nil, err + } + return &gotToken, nil +} + +type TokenCache struct { + expiration time.Duration + cache map[string]*tokenIssuerCache + logger log.Logger +} + +func NewTokenCache(clients map[string]*Client, expiration time.Duration, logger log.Logger) *TokenCache { + tc := &TokenCache{ + expiration: expiration, + logger: logger, + } + tc.cache = make(map[string]*tokenIssuerCache) + for issuer, client := range clients { + tc.cache[issuer] = newTokenIssuerCache(client) + } + return tc +} + +func (t *TokenCache) getValidUntil(token *pb.Token) time.Time { + blacklisted := token.GetBlacklisted().GetFlag() + if blacklisted { + expiration := token.GetExpiration() + if expiration == 0 { + return time.Time{} + } + return time.Unix(expiration, 0) + } + + if t.expiration == 0 { + return time.Time{} + } + return time.Now().Add(t.expiration) +} + +func getTokenUUID(tokenClaims jwt.Claims) (string, uuid.UUID, error) { + tokenID, err := getID(tokenClaims) + if err != nil { + return "", uuid.Nil, err + } + tokenUUID, err := uuid.Parse(tokenID) + if err != nil { + return "", uuid.Nil, err + } + return tokenID, tokenUUID, nil +} + +func (t *TokenCache) VerifyTrust(ctx context.Context, issuer, token string, tokenClaims jwt.Claims) error { + tc, ok := t.cache[issuer] + if !ok { + t.logger.Debugf("client not set for issuer %v, trust verification skipped", issuer) + return nil + } + + tokenID, tokenUUID, err := getTokenUUID(tokenClaims) + if err != nil { + return err + } + t.logger.Debugf("checking trust for issuer(%v) for token(id=%s)", issuer, tokenID) + tf, set := tc.getValidTokenRecordOrFuture(tokenUUID) + if set == nil { + tv, errG := tf.Get(ctx) + if errG != nil { + return errG + } + t.logger.Debugf("token(id=%s) found in cache (blacklisted=%v, validUntil=%v)", tokenID, tv.blacklisted, tv.validUntil.Load()) + if tv.blacklisted { + return ErrBlackListedToken + } + return nil + } + + t.logger.Debugf("requesting token(id=%s) verification by m2m", tokenID) + respToken, err := tc.verifyTokenByRequest(ctx, token, tokenID) + if err != nil { + tc.removeTokenRecordAndSetErrorOnFuture(tokenUUID, set, err) + return err + } + + var onExpire func(uuid.UUID) + if t.logger.Check(log.DebugLevel) { + onExpire = func(tid uuid.UUID) { + t.logger.Debugf("token(id=%s) expired", tid.String()) + } + } + + blacklisted := respToken.GetBlacklisted().GetFlag() + validUntil := t.getValidUntil(respToken) + tr := newTokenRecord(blacklisted, validUntil, onExpire) + t.logger.Debugf("token(id=%s) set (blacklisted=%v, validUntil=%v)", tokenID, blacklisted, validUntil) + tc.setTokenRecordAndWaitingFuture(tokenUUID, tr, set) + + if blacklisted { + return ErrBlackListedToken + } + return nil +} + +func (t *TokenCache) CheckExpirations(now time.Time) { + for _, ic := range t.cache { + ic.checkExpirations(now) + } +} diff --git a/pkg/security/jwt/tokenCache_internal_test.go b/pkg/security/jwt/tokenCache_internal_test.go new file mode 100644 index 000000000..e0e8ec240 --- /dev/null +++ b/pkg/security/jwt/tokenCache_internal_test.go @@ -0,0 +1,141 @@ +package jwt + +import ( + "context" + "errors" + "net/http" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const TEST_TIMEOUT = time.Second * 10 + +func TestTokenRecord_IsExpired(t *testing.T) { + now := time.Now() + tests := []struct { + name string + recordTime time.Time + want bool + }{ + {"Not expired", now.Add(time.Hour), false}, + {"Expired", now.Add(-time.Hour), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + record := newTokenRecord(false, tt.recordTime, nil) + require.Equal(t, tt.want, record.IsExpired(now)) + }) + } +} + +func TestTokenIssuerCacheSetAndGetToken(t *testing.T) { + cache := newTokenIssuerCache(&Client{Client: &http.Client{}, tokenEndpoint: "http://example.com"}) + + ctx, cancel := context.WithTimeout(context.Background(), TEST_TIMEOUT) + defer cancel() + tokenID := uuid.New() + // token doesn't exist yet, so we should get a future with a set function + _, setTokenOrError := cache.getValidTokenRecordOrFuture(tokenID) + require.NotNil(t, setTokenOrError) + + // we can wait on the future in other goroutine + // -> setting error on the future should unblock the goroutine + waiting := make(chan struct{}) + done := make(chan struct{}) + go func() { + defer close(done) + tf2, setToken2 := cache.getValidTokenRecordOrFuture(tokenID) + assert.Nil(t, setToken2) + close(waiting) + _, err := tf2.Get(ctx) + assert.Error(t, err) + }() + + <-waiting + cache.removeTokenRecordAndSetErrorOnFuture(tokenID, setTokenOrError, errors.New("test")) + select { + case <-done: + case <-ctx.Done(): + require.Fail(t, "timeout") + } + + // get a new future + _, setTokenOrError = cache.getValidTokenRecordOrFuture(tokenID) + require.NotNil(t, setTokenOrError) + + // -> setting an expired token record should result in a future with a set function being returned + expiredIDs := []uuid.UUID{} + tr := newTokenRecord(false, time.Now().Add(-time.Hour), func(u uuid.UUID) { + expiredIDs = append(expiredIDs, u) + }) + cache.setTokenRecord(tokenID, tr) + _, setTokenOrError = cache.getValidTokenRecordOrFuture(tokenID) + require.NotNil(t, setTokenOrError) + require.Len(t, expiredIDs, 1) + require.Equal(t, tokenID, expiredIDs[0]) + + // -> finally, set valid token record + tr = newTokenRecord(false, time.Now().Add(time.Hour), nil) + waiting = make(chan struct{}) + done = make(chan struct{}) + go func() { + defer close(done) + tf2, setToken2 := cache.getValidTokenRecordOrFuture(tokenID) + assert.Nil(t, setToken2) + close(waiting) + result, err := tf2.Get(ctx) + assert.NoError(t, err) + assert.Equal(t, tr, result) + }() + + <-waiting + cache.setTokenRecordAndWaitingFuture(tokenID, tr, setTokenOrError) + select { + case <-done: + case <-ctx.Done(): + require.Fail(t, "timeout") + } + + // cache should return a token record now, not a future + tf, _ := cache.getValidTokenRecordOrFuture(tokenID) + _, ok := tf.tokenOrFuture.(*tokenRecord) + require.True(t, ok) +} + +func TestTokenIssuerCacheCheckExpirations(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), TEST_TIMEOUT) + defer cancel() + cache := newTokenIssuerCache(&Client{Client: &http.Client{}, tokenEndpoint: "http://example.com"}) + + now := time.Now() + tokenID1 := uuid.New() + expiredIDs := []uuid.UUID{} + onExpire := func(u uuid.UUID) { + expiredIDs = append(expiredIDs, u) + } + tokenRecord1 := newTokenRecord(false, now.Add(-time.Hour), onExpire) + cache.setTokenRecord(tokenID1, tokenRecord1) + + tokenID2 := uuid.New() + tokenRecord2 := newTokenRecord(false, now.Add(time.Hour), onExpire) + cache.setTokenRecord(tokenID2, tokenRecord2) + + cache.checkExpirations(now) + + // tokenRecord1 should have been removed and we should get a future with a set function + _, setTf1 := cache.getValidTokenRecordOrFuture(tokenID1) + require.NotNil(t, setTf1) + require.Len(t, expiredIDs, 1) + require.Equal(t, tokenID1, expiredIDs[0]) + // tokenRecord2 should still be there + tf2, setTf2 := cache.getValidTokenRecordOrFuture(tokenID2) + require.Nil(t, setTf2) + result, err := tf2.Get(ctx) + require.NoError(t, err) + require.Equal(t, tokenRecord2, result) +} diff --git a/pkg/security/jwt/validator.go b/pkg/security/jwt/validator.go index 2eb887af2..e2f0fc63e 100644 --- a/pkg/security/jwt/validator.go +++ b/pkg/security/jwt/validator.go @@ -4,8 +4,12 @@ import ( "context" "errors" "fmt" + "time" "github.com/golang-jwt/jwt/v5" + "github.com/plgd-dev/go-coap/v3/pkg/runner/periodic" + "github.com/plgd-dev/hub/v2/pkg/log" + pkgHttpUri "github.com/plgd-dev/hub/v2/pkg/net/http/uri" ) type KeyCacheI interface { @@ -14,16 +18,62 @@ type KeyCacheI interface { } type Validator struct { - keys KeyCacheI + keys KeyCacheI + verifyTrust bool + tokenCache *TokenCache } var ( - ErrMissingToken = errors.New("missing token") - ErrCannotParseToken = errors.New("could not parse token") + ErrMissingToken = errors.New("missing token") + ErrCannotParseToken = errors.New("could not parse token") + ErrCannotVerifyTrust = errors.New("could not verify token trust") + ErrBlackListedToken = errors.New("token is blacklisted") ) -func NewValidator(keyCache KeyCacheI) *Validator { - return &Validator{keys: keyCache} +type config struct { + verifyTrust bool + clients map[string]*Client + cacheExpiration time.Duration + stop <-chan struct{} +} + +type Option interface { + apply(*config) +} + +type optionFunc func(*config) + +func (o optionFunc) apply(c *config) { + o(c) +} + +func WithTrustVerification(clients map[string]*Client, cacheExpiration time.Duration, stop <-chan struct{}) Option { + return optionFunc(func(c *config) { + c.verifyTrust = true + c.clients = clients + c.cacheExpiration = cacheExpiration + c.stop = stop + }) +} + +func NewValidator(keyCache KeyCacheI, logger log.Logger, opts ...Option) *Validator { + c := &config{} + for _, opt := range opts { + opt.apply(c) + } + v := &Validator{ + keys: keyCache, + verifyTrust: c.verifyTrust, + } + if c.verifyTrust && len(c.clients) > 0 { + v.tokenCache = NewTokenCache(c.clients, c.cacheExpiration, logger) + add := periodic.New(c.stop, c.cacheExpiration/2) + add(func(now time.Time) bool { + v.tokenCache.CheckExpirations(now) + return true + }) + } + return v } func errParseToken(err error) error { @@ -34,10 +84,23 @@ func errParseTokenInvalidClaimsType(t *jwt.Token) error { return fmt.Errorf("%w: unsupported type %T", ErrCannotParseToken, t.Claims) } +func (v *Validator) checkTrust(ctx context.Context, token string, claims jwt.Claims) error { + issuer, err := claims.GetIssuer() + if err != nil { + return err + } + issuer = pkgHttpUri.CanonicalURI(issuer) + return v.tokenCache.VerifyTrust(ctx, issuer, token, claims) +} + func (v *Validator) Parse(token string) (jwt.MapClaims, error) { return v.ParseWithContext(context.Background(), token) } +func errVerifyTrust(err error) error { + return fmt.Errorf("%w: %w", ErrCannotVerifyTrust, err) +} + func (v *Validator) ParseWithContext(ctx context.Context, token string) (jwt.MapClaims, error) { if token == "" { return nil, ErrMissingToken @@ -51,15 +114,19 @@ func (v *Validator) ParseWithContext(ctx context.Context, token string) (jwt.Map if err != nil { return nil, errParseToken(err) } - c, ok := t.Claims.(jwt.MapClaims) + claims, ok := t.Claims.(jwt.MapClaims) if !ok { return nil, errParseTokenInvalidClaimsType(t) } - - return c, nil + if v.verifyTrust { + if err = v.checkTrust(ctx, token, claims); err != nil { + return nil, errVerifyTrust(err) + } + } + return claims, nil } -func (v *Validator) ParseWithClaims(token string, claims jwt.Claims) error { +func (v *Validator) ParseWithClaims(ctx context.Context, token string, claims jwt.Claims) error { if token == "" { return ErrMissingToken } @@ -68,5 +135,10 @@ func (v *Validator) ParseWithClaims(token string, claims jwt.Claims) error { if err != nil { return errParseToken(err) } + if v.verifyTrust { + if err = v.checkTrust(ctx, token, claims); err != nil { + return errVerifyTrust(err) + } + } return nil } diff --git a/pkg/security/jwt/validator/config.go b/pkg/security/jwt/validator/config.go index 6bad3555a..9a971f1c5 100644 --- a/pkg/security/jwt/validator/config.go +++ b/pkg/security/jwt/validator/config.go @@ -2,6 +2,7 @@ package validator import ( "fmt" + "time" "github.com/plgd-dev/hub/v2/pkg/net/http/client" ) @@ -21,11 +22,24 @@ func (c *AuthorityConfig) Validate() error { return nil } +type TokenTrustVerificationConfig struct { + Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` + CacheExpiration time.Duration `yaml:"cacheExpiration,omitempty" json:"cacheExpiration,omitempty"` +} + +func (c *TokenTrustVerificationConfig) Validate() error { + if c.Enabled && c.CacheExpiration == 0 { + return fmt.Errorf("cacheExpiration('%v')", c.CacheExpiration) + } + return nil +} + type Config struct { - Audience string `yaml:"audience" json:"audience"` - Endpoints []AuthorityConfig `yaml:"endpoints" json:"endpoints"` - Authority *string `yaml:"authority,omitempty" json:"authority,omitempty"` // deprecated - HTTP *client.Config `yaml:"http,omitempty" json:"http,omitempty"` // deprecated + Audience string `yaml:"audience" json:"audience"` + Endpoints []AuthorityConfig `yaml:"endpoints" json:"endpoints"` + TokenVerification TokenTrustVerificationConfig `yaml:"tokenTrustVerification,omitempty" json:"tokenTrustVerification,omitempty"` + Authority *string `yaml:"authority,omitempty" json:"authority,omitempty"` // deprecated + HTTP *client.Config `yaml:"http,omitempty" json:"http,omitempty"` // deprecated } func (c *Config) Validate() error { @@ -45,5 +59,5 @@ func (c *Config) Validate() error { return fmt.Errorf("endpoints[%v].%w", i, err) } } - return nil + return c.TokenVerification.Validate() } diff --git a/pkg/security/jwt/validator/validator.go b/pkg/security/jwt/validator/validator.go index 30036f3a4..d0064d513 100644 --- a/pkg/security/jwt/validator/validator.go +++ b/pkg/security/jwt/validator/validator.go @@ -2,13 +2,16 @@ package validator import ( "context" + "errors" "fmt" + "net/http" "github.com/golang-jwt/jwt/v5" "github.com/plgd-dev/hub/v2/pkg/fn" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" "github.com/plgd-dev/hub/v2/pkg/net/http/client" + pkgHttpUri "github.com/plgd-dev/hub/v2/pkg/net/http/uri" jwtValidator "github.com/plgd-dev/hub/v2/pkg/security/jwt" "github.com/plgd-dev/hub/v2/pkg/security/openid" "go.opentelemetry.io/otel/trace" @@ -38,10 +41,30 @@ func (v *Validator) GetParser() *jwtValidator.Validator { return v.validator } -func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*Validator, error) { +type GetOpenIDConfigurationFunc func(ctx context.Context, c *http.Client, authority string) (openid.Config, error) + +type Options struct { + getOpenIDConfiguration GetOpenIDConfigurationFunc +} + +func WithGetOpenIDConfiguration(f GetOpenIDConfigurationFunc) func(o *Options) { + return func(o *Options) { + o.getOpenIDConfiguration = f + } +} + +func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider, opts ...func(o *Options)) (*Validator, error) { + options := Options{ + getOpenIDConfiguration: openid.GetConfiguration, + } + for _, o := range opts { + o(&options) + } + keys := jwtValidator.NewMultiKeyCache() var onClose fn.FuncList openIDConfigurations := make([]openid.Config, 0, len(config.Endpoints)) + clients := make(map[string]*jwtValidator.Client, len(config.Endpoints)) for _, authority := range config.Endpoints { httpClient, err := client.New(authority.HTTP, fileWatcher, logger, tracerProvider) if err != nil { @@ -51,20 +74,33 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg ctx2, cancel := context.WithTimeout(ctx, authority.HTTP.Timeout) defer cancel() - openIDCfg, err := openid.GetConfiguration(ctx2, httpClient.HTTP(), authority.Authority) + if options.getOpenIDConfiguration == nil { + return nil, errors.New("GetOpenIDConfiguration is nil") + } + + openIDCfg, err := options.getOpenIDConfiguration(ctx2, httpClient.HTTP(), authority.Authority) if err != nil { onClose.Execute() httpClient.Close() return nil, fmt.Errorf("cannot get openId configuration: %w", err) } onClose.AddFunc(httpClient.Close) - keys.Add(openIDCfg.Issuer, openIDCfg.JWKSURL, httpClient.HTTP()) + issuer := pkgHttpUri.CanonicalURI(openIDCfg.Issuer) + keys.Add(issuer, openIDCfg.JWKSURL, httpClient.HTTP()) openIDConfigurations = append(openIDConfigurations, openIDCfg) + if config.TokenVerification.Enabled && openIDCfg.PlgdTokensEndpoint != "" { + clients[issuer] = jwtValidator.NewClient(httpClient.HTTP(), openIDCfg.PlgdTokensEndpoint) + } + } + + var vopts []jwtValidator.Option + if len(clients) > 0 { + vopts = append(vopts, jwtValidator.WithTrustVerification(clients, config.TokenVerification.CacheExpiration, ctx.Done())) } return &Validator{ openIDConfigurations: openIDConfigurations, - validator: jwtValidator.NewValidator(keys), + validator: jwtValidator.NewValidator(keys, logger, vopts...), audience: config.Audience, }, nil } @@ -77,6 +113,6 @@ func (v *Validator) OpenIDConfiguration() []openid.Config { return v.openIDConfigurations } -func (v *Validator) ParseWithClaims(token string, claims jwt.Claims) error { - return v.validator.ParseWithClaims(token, claims) +func (v *Validator) ParseWithClaims(ctx context.Context, token string, claims jwt.Claims) error { + return v.validator.ParseWithClaims(ctx, token, claims) } diff --git a/pkg/security/jwt/validator_test.go b/pkg/security/jwt/validator_test.go index baef741e4..aed381e0b 100644 --- a/pkg/security/jwt/validator_test.go +++ b/pkg/security/jwt/validator_test.go @@ -49,7 +49,7 @@ func TestValidator(t *testing.T) { v := test.GetJWTValidator(jwks.URL()) var c testClaims - err := v.ParseWithClaims(testToken(t), &c) + err := v.ParseWithClaims(context.Background(), testToken(t), &c) require.NoError(t, err) assert.Equal(t, "test.client.id", c.ClientID) @@ -63,7 +63,7 @@ func TestClaims(t *testing.T) { v := test.GetJWTValidator(jwks.URL()) var c pkgJwt.Claims - err := v.ParseWithClaims(testToken(t), &c) + err := v.ParseWithClaims(context.Background(), testToken(t), &c) require.ErrorIs(t, err, jwt.ErrTokenExpired) clientID, err := c.GetClientID() @@ -133,7 +133,7 @@ func TestEmptyToken(t *testing.T) { require.ErrorIs(t, err, pkgJwt.ErrMissingToken) var c pkgJwt.Claims - err = v.ParseWithClaims("", &c) + err = v.ParseWithClaims(context.Background(), "", &c) require.ErrorIs(t, err, pkgJwt.ErrMissingToken) _, err = v.ParseWithContext(context.Background(), "") @@ -151,6 +151,6 @@ func TestInvalidToken(t *testing.T) { require.ErrorIs(t, err, pkgJwt.ErrCannotParseToken) var c pkgJwt.Claims - err = v.ParseWithClaims("invalid", &c) + err = v.ParseWithClaims(context.Background(), "invalid", &c) require.ErrorIs(t, err, pkgJwt.ErrCannotParseToken) } diff --git a/pkg/security/openid/config.go b/pkg/security/openid/config.go index 448fe8761..f66f4044a 100644 --- a/pkg/security/openid/config.go +++ b/pkg/security/openid/config.go @@ -10,6 +10,7 @@ type Config struct { UserInfoURL string `json:"userinfo_endpoint,omitempty"` Algorithms []string `json:"id_token_signing_alg_values_supported,omitempty"` EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + PlgdTokensEndpoint string `json:"plgd_tokens_endpoint,omitempty"` } func (c Config) Validate() error { diff --git a/pkg/sync/task/future/future_test.go b/pkg/sync/task/future/future_test.go index 452a9edd9..cd34eb6f0 100644 --- a/pkg/sync/task/future/future_test.go +++ b/pkg/sync/task/future/future_test.go @@ -7,11 +7,12 @@ import ( "testing" "time" - "github.com/plgd-dev/hub/v2/test/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +const TEST_TIMEOUT = time.Second * 10 + func TestFutureReady(t *testing.T) { fut, set := New() require.False(t, fut.Ready()) @@ -50,7 +51,7 @@ func TestFutureGetTimeout(t *testing.T) { func TestFutureGetMultithreaded(t *testing.T) { fut, set := New() - ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + ctx, cancel := context.WithTimeout(context.Background(), TEST_TIMEOUT) defer cancel() const val = "test" @@ -72,7 +73,7 @@ func TestFutureGetMultithreaded(t *testing.T) { func TestFutureGetAfterSet(t *testing.T) { fut, set := New() - ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + ctx, cancel := context.WithTimeout(context.Background(), TEST_TIMEOUT) defer cancel() const val = "test" @@ -84,7 +85,7 @@ func TestFutureGetAfterSet(t *testing.T) { } func TestFutureSetFromWorkerRoutine(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + ctx, cancel := context.WithTimeout(context.Background(), TEST_TIMEOUT) defer cancel() fut, set := New() @@ -100,7 +101,7 @@ func TestFutureSetFromWorkerRoutine(t *testing.T) { } func TestFutureRepeatedSet(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + ctx, cancel := context.WithTimeout(context.Background(), TEST_TIMEOUT) defer cancel() fut, set := New() diff --git a/pkg/time/unixnano.go b/pkg/time/unixnano.go index d25d7e356..98018b174 100644 --- a/pkg/time/unixnano.go +++ b/pkg/time/unixnano.go @@ -16,6 +16,14 @@ func UnixNano(t time.Time) int64 { return v } +func UnixSec(t time.Time) int64 { + v := int64(0) + if !t.IsZero() { + v = t.Unix() + } + return v +} + func Unix(sec int64, nsec int64) time.Time { if sec != 0 || nsec != 0 { return time.Unix(sec, nsec) diff --git a/resource-aggregate/config.yaml b/resource-aggregate/config.yaml index 9a33922b2..fd1185ee6 100644 --- a/resource-aggregate/config.yaml +++ b/resource-aggregate/config.yaml @@ -45,6 +45,9 @@ apis: keyFile: "/secrets/private/cert.key" certFile: "/secrets/public/cert.crt" useSystemCAPool: false + tokenTrustVerification: + enabled: true + cacheExpiration: 30s clients: eventBus: nats: diff --git a/resource-aggregate/events/resourceChanged.go b/resource-aggregate/events/resourceChanged.go index 91cb5d649..2ea7a4437 100644 --- a/resource-aggregate/events/resourceChanged.go +++ b/resource-aggregate/events/resourceChanged.go @@ -74,6 +74,10 @@ func (rc *ResourceChanged) CopyData(event *ResourceChanged) { rc.ResourceTypes = event.GetResourceTypes() } +func (rc *ResourceChanged) Clone() *ResourceChanged { + return proto.Clone(rc).(*ResourceChanged) +} + func (rc *ResourceChanged) CheckInitialized() bool { return rc.GetResourceId() != nil && rc.GetContent() != nil && diff --git a/resource-aggregate/service/service_test.go b/resource-aggregate/service/service_test.go index 598364b79..81419e721 100644 --- a/resource-aggregate/service/service_test.go +++ b/resource-aggregate/service/service_test.go @@ -7,7 +7,6 @@ import ( "github.com/plgd-dev/device/v2/schema/platform" pbIS "github.com/plgd-dev/hub/v2/identity-store/pb" - idService "github.com/plgd-dev/hub/v2/identity-store/test" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" @@ -17,6 +16,7 @@ import ( "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + hubTestService "github.com/plgd-dev/hub/v2/test/service" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" ) @@ -27,18 +27,13 @@ func TestPublishUnpublish(t *testing.T) { fmt.Println("cfg: ", cfg) - oauthShutdown := oauthTest.SetUp(t) - defer oauthShutdown() + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + const services = hubTestService.SetUpServicesMachine2MachineOAuth | hubTestService.SetUpServicesOAuth | hubTestService.SetUpServicesId | hubTestService.SetUpServicesResourceAggregate + tearDown := hubTestService.SetUpServices(ctx, t, services, hubTestService.WithRAConfig(cfg)) + defer tearDown() - idShutdown := idService.SetUp(t) - defer idShutdown() - logCfg := log.MakeDefaultConfig() - logCfg.Level = log.DebugLevel - log.Setup(logCfg) - raShutdown := raTest.New(t, cfg) - defer raShutdown() - - ctx := kitNetGrpc.CtxWithToken(context.Background(), oauthTest.GetDefaultAccessToken(t)) + ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) fileWatcher, err := fsnotify.NewWatcher(log.Get()) require.NoError(t, err) @@ -74,8 +69,6 @@ func TestPublishUnpublish(t *testing.T) { require.NoError(t, err) }() - logCfg.Level = log.DebugLevel - log.Setup(logCfg) pubReq := testMakePublishResourceRequest(deviceID, []string{href}) _, err = raClient.PublishResourceLinks(ctx, pubReq) require.NoError(t, err) diff --git a/resource-directory/config.yaml b/resource-directory/config.yaml index 502ccd8c3..0f68f9d03 100644 --- a/resource-directory/config.yaml +++ b/resource-directory/config.yaml @@ -45,6 +45,9 @@ apis: keyFile: "/secrets/private/cert.key" certFile: "/secrets/public/cert.crt" useSystemCAPool: false + tokenTrustVerification: + enabled: true + cacheExpiration: 30s clients: eventBus: # number of routines to process events in projection diff --git a/resource-directory/service/getPendingCommands_test.go b/resource-directory/service/getPendingCommands_test.go index 16228fefe..6ba80b681 100644 --- a/resource-directory/service/getPendingCommands_test.go +++ b/resource-directory/service/getPendingCommands_test.go @@ -11,16 +11,12 @@ import ( "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/schema/platform" "github.com/plgd-dev/go-coap/v3/message" - caService "github.com/plgd-dev/hub/v2/certificate-authority/test" coapgwTest "github.com/plgd-dev/hub/v2/coap-gateway/test" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" - grpcgwService "github.com/plgd-dev/hub/v2/grpc-gateway/test" - idService "github.com/plgd-dev/hub/v2/identity-store/test" + "github.com/plgd-dev/hub/v2/pkg/fn" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" - raService "github.com/plgd-dev/hub/v2/resource-aggregate/test" - rdService "github.com/plgd-dev/hub/v2/resource-directory/test" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" "github.com/plgd-dev/hub/v2/test/oauth-server/service" @@ -329,20 +325,19 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { defer cancel() testService.ClearDB(ctx, t) - oauthShutdown := oauthTest.SetUp(t) - authShutdown := idService.SetUp(t) - raShutdown := raService.SetUp(t) - rdShutdown := rdService.SetUp(t) - grpcShutdown := grpcgwService.SetUp(t) - caShutdown := caService.SetUp(t) - secureGWShutdown := coapgwTest.SetUp(t) + var closeFunc fn.FuncList + defer closeFunc.Execute() + tearDown := testService.SetUpServices(ctx, t, testService.SetUpServicesOAuth|testService.SetUpServicesMachine2MachineOAuth|testService.SetUpServicesId|testService.SetUpServicesResourceAggregate| + testService.SetUpServicesResourceDirectory|testService.SetUpServicesCertificateAuthority|testService.SetUpServicesGrpcGateway) + closeFunc.AddFunc(tearDown) - defer caShutdown() - defer grpcShutdown() - defer rdShutdown() - defer raShutdown() - defer authShutdown() - defer oauthShutdown() + deferedSecureGWShutdown := true + secureGWShutdown := coapgwTest.SetUp(t) + defer func() { + if deferedSecureGWShutdown { + secureGWShutdown() + } + }() ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) @@ -358,6 +353,7 @@ func TestRequestHandlerGetPendingCommands(t *testing.T) { deviceID, shutdownDevSim := test.OnboardDevSim(ctx, t, c, deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, test.GetAllBackendResourceLinks()) defer shutdownDevSim() + deferedSecureGWShutdown = false secureGWShutdown() createFn := func(timeToLive time.Duration) { diff --git a/snippet-service/config.yaml b/snippet-service/config.yaml index 32976d9a2..177c0890e 100644 --- a/snippet-service/config.yaml +++ b/snippet-service/config.yaml @@ -44,6 +44,9 @@ apis: keyFile: "/secrets/private/cert.key" certFile: "/secrets/public/cert.crt" useSystemCAPool: false + tokenTrustVerification: + enabled: true + cacheExpiration: 30s http: address: "0.0.0.0:9101" readTimeout: 8s diff --git a/snippet-service/pb/appliedConfiguration.go b/snippet-service/pb/appliedConfiguration.go index 813be74dd..02ddf05c4 100644 --- a/snippet-service/pb/appliedConfiguration.go +++ b/snippet-service/pb/appliedConfiguration.go @@ -92,8 +92,11 @@ func (r *AppliedConfiguration_Resource) Clone() *AppliedConfiguration_Resource { } } -func (r *AppliedConfiguration_Resource) jsonToBSONTag(json map[string]interface{}) { - pkgMongo.ConvertStringValueToInt64(json, "validUntil") +func (r *AppliedConfiguration_Resource) jsonToBSONTag(json map[string]interface{}) error { + if _, err := pkgMongo.ConvertStringValueToInt64(json, true, "."+ValidUntil); err != nil { + return fmt.Errorf("cannot convert .validUntil to int64: %w", err) + } + return nil } func (r *AppliedConfiguration_Resource) MarshalBSON() ([]byte, error) { @@ -133,10 +136,17 @@ func (c *AppliedConfiguration) Clone() *AppliedConfiguration { } } -func (c *AppliedConfiguration) jsonToBSONTag(json map[string]interface{}) { - pkgMongo.ConvertStringValueToInt64(json, "configurationId.version") - pkgMongo.ConvertStringValueToInt64(json, "conditionId.version") - pkgMongo.ConvertStringValueToInt64(json, "resources.validUntil") +func (c *AppliedConfiguration) jsonToBSONTag(json map[string]interface{}) error { + if _, err := pkgMongo.ConvertStringValueToInt64(json, true, "."+ConfigurationIDKey+"."+VersionKey); err != nil { + return fmt.Errorf("cannot convert configurationId.version to int64: %w", err) + } + if _, err := pkgMongo.ConvertStringValueToInt64(json, true, "."+ConditionIDKey+"."+VersionKey); err != nil { + return fmt.Errorf("cannot convert conditionId.version to int64: %w", err) + } + if _, err := pkgMongo.ConvertStringValueToInt64(json, true, "."+ResourcesKey+".[*]."+ValidUntil); err != nil { + return fmt.Errorf("cannot convert resources.validUntil to int64: %w", err) + } + return nil } func (c *AppliedConfiguration) MarshalBSON() ([]byte, error) { diff --git a/snippet-service/pb/service.pb.go b/snippet-service/pb/service.pb.go index 7f23b6748..ab33dcf66 100644 --- a/snippet-service/pb/service.pb.go +++ b/snippet-service/pb/service.pb.go @@ -765,8 +765,8 @@ type AppliedConfiguration struct { unknownFields protoimpl.UnknownFields Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - DeviceId string `protobuf:"bytes,2,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty" bson:"deviceId"` - ConfigurationId *AppliedConfiguration_LinkedTo `protobuf:"bytes,3,opt,name=configuration_id,json=configurationId,proto3" json:"configuration_id,omitempty" bson:"configurationId"` + DeviceId string `protobuf:"bytes,2,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty" bson:"deviceId"` + ConfigurationId *AppliedConfiguration_LinkedTo `protobuf:"bytes,3,opt,name=configuration_id,json=configurationId,proto3" json:"configuration_id,omitempty" bson:"configurationId"` // Types that are assignable to ExecutedBy: // // *AppliedConfiguration_OnDemand @@ -882,7 +882,7 @@ type AppliedConfiguration_OnDemand struct { } type AppliedConfiguration_ConditionId struct { - ConditionId *AppliedConfiguration_LinkedTo `protobuf:"bytes,5,opt,name=condition_id,json=conditionId,proto3,oneof" bson:"conditionId"` + ConditionId *AppliedConfiguration_LinkedTo `protobuf:"bytes,5,opt,name=condition_id,json=conditionId,proto3,oneof" bson:"conditionId"` } func (*AppliedConfiguration_OnDemand) isAppliedConfiguration_ExecutedBy() {} @@ -1269,11 +1269,11 @@ type AppliedConfiguration_Resource struct { Href string `protobuf:"bytes,1,opt,name=href,proto3" json:"href,omitempty"` // Reused from invoke command or generated. Can be used to retrieve corresponding pending command. - CorrelationId string `protobuf:"bytes,2,opt,name=correlation_id,json=correlationId,proto3" json:"correlation_id,omitempty" bson:"correlationId"` + CorrelationId string `protobuf:"bytes,2,opt,name=correlation_id,json=correlationId,proto3" json:"correlation_id,omitempty" bson:"correlationId"` Status AppliedConfiguration_Resource_Status `protobuf:"varint,3,opt,name=status,proto3,enum=snippetservice.pb.AppliedConfiguration_Resource_Status" json:"status,omitempty"` - ResourceUpdated *events.ResourceUpdated `protobuf:"bytes,4,opt,name=resource_updated,json=resourceUpdated,proto3" json:"resource_updated,omitempty" bson:"resourceUpdated,omitempty"` + ResourceUpdated *events.ResourceUpdated `protobuf:"bytes,4,opt,name=resource_updated,json=resourceUpdated,proto3" json:"resource_updated,omitempty" bson:"resourceUpdated,omitempty"` // Unix nanoseconds timestamp for resource in PENDING status, until which the pending update is valid - ValidUntil int64 `protobuf:"varint,5,opt,name=valid_until,json=validUntil,proto3" json:"valid_until,omitempty" bson:"validUntil,omitempty"` + ValidUntil int64 `protobuf:"varint,5,opt,name=valid_until,json=validUntil,proto3" json:"valid_until,omitempty" bson:"validUntil,omitempty"` } func (x *AppliedConfiguration_Resource) Reset() { diff --git a/snippet-service/service/http/createCondition_test.go b/snippet-service/service/http/createCondition_test.go index 4d85fb4ba..44f0baf03 100644 --- a/snippet-service/service/http/createCondition_test.go +++ b/snippet-service/service/http/createCondition_test.go @@ -9,6 +9,7 @@ import ( "github.com/google/uuid" "github.com/plgd-dev/go-coap/v3/message" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" snippetPb "github.com/plgd-dev/hub/v2/snippet-service/pb" snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" snippetTest "github.com/plgd-dev/hub/v2/snippet-service/test" @@ -40,7 +41,7 @@ func TestRequestHandlerCreateCondition(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth) defer shutDown() snippetCfg := snippetTest.MakeConfig(t) @@ -136,7 +137,7 @@ func TestRequestHandlerCreateCondition(t *testing.T) { require.Equal(t, tt.wantHTTPCode, resp.StatusCode) var got snippetPb.Condition - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) if tt.wantErr { require.Error(t, err) return diff --git a/snippet-service/service/http/createConfiguration_test.go b/snippet-service/service/http/createConfiguration_test.go index c54472cb2..79a75a365 100644 --- a/snippet-service/service/http/createConfiguration_test.go +++ b/snippet-service/service/http/createConfiguration_test.go @@ -10,6 +10,7 @@ import ( "github.com/plgd-dev/go-coap/v3/message" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" snippetPb "github.com/plgd-dev/hub/v2/snippet-service/pb" snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" @@ -39,7 +40,7 @@ func TestRequestHandlerCreateConfiguration(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth) defer shutDown() snippetCfg := snippetTest.MakeConfig(t) @@ -188,7 +189,7 @@ func TestRequestHandlerCreateConfiguration(t *testing.T) { require.Equal(t, tt.wantHTTPCode, resp.StatusCode) var got snippetPb.Configuration - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) if tt.wantErr { require.Error(t, err) return diff --git a/snippet-service/service/http/deleteAppliedConfigurations_test.go b/snippet-service/service/http/deleteAppliedConfigurations_test.go index 745c12420..32f52c5e8 100644 --- a/snippet-service/service/http/deleteAppliedConfigurations_test.go +++ b/snippet-service/service/http/deleteAppliedConfigurations_test.go @@ -11,6 +11,7 @@ import ( "github.com/plgd-dev/go-coap/v3/message" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/snippet-service/pb" snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" "github.com/plgd-dev/hub/v2/snippet-service/test" @@ -28,7 +29,7 @@ func TestRequestHandlerDeleteAppliedConfigurations(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth) defer shutDown() snippetCfg := test.MakeConfig(t) @@ -141,7 +142,7 @@ func TestRequestHandlerDeleteAppliedConfigurations(t *testing.T) { require.Equal(t, tt.wantHTTPCode, resp.StatusCode) var deleteResp pb.DeleteAppliedConfigurationsResponse - err := httpTest.Unmarshal(resp.StatusCode, resp.Body, &deleteResp) + err := pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &deleteResp) if tt.wantErr { require.Error(t, err) return diff --git a/snippet-service/service/http/deleteConditions_test.go b/snippet-service/service/http/deleteConditions_test.go index 40863d348..760cf8a5b 100644 --- a/snippet-service/service/http/deleteConditions_test.go +++ b/snippet-service/service/http/deleteConditions_test.go @@ -10,6 +10,7 @@ import ( "github.com/plgd-dev/go-coap/v3/message" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/snippet-service/pb" snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" "github.com/plgd-dev/hub/v2/snippet-service/test" @@ -28,7 +29,7 @@ func TestRequestHandlerDeleteConditions(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth) defer shutDown() snippetCfg := test.MakeConfig(t) @@ -136,7 +137,7 @@ func TestRequestHandlerDeleteConditions(t *testing.T) { require.Equal(t, tt.wantHTTPCode, resp.StatusCode) var deleteResp pb.DeleteConditionsResponse - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &deleteResp) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &deleteResp) if tt.wantErr { require.Error(t, err) return diff --git a/snippet-service/service/http/deleteConfigurations_test.go b/snippet-service/service/http/deleteConfigurations_test.go index 0417087f9..4ebd0b1e2 100644 --- a/snippet-service/service/http/deleteConfigurations_test.go +++ b/snippet-service/service/http/deleteConfigurations_test.go @@ -10,6 +10,7 @@ import ( "github.com/plgd-dev/go-coap/v3/message" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/snippet-service/pb" snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" "github.com/plgd-dev/hub/v2/snippet-service/test" @@ -28,7 +29,7 @@ func TestRequestHandlerDeleteConfigurations(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth) defer shutDown() snippetCfg := test.MakeConfig(t) @@ -136,7 +137,7 @@ func TestRequestHandlerDeleteConfigurations(t *testing.T) { require.Equal(t, tt.wantHTTPCode, resp.StatusCode) var deleteResp pb.DeleteConfigurationsResponse - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &deleteResp) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &deleteResp) if tt.wantErr { require.Error(t, err) return diff --git a/snippet-service/service/http/getAppliedConfigurations_test.go b/snippet-service/service/http/getAppliedConfigurations_test.go index de140f162..20d96f469 100644 --- a/snippet-service/service/http/getAppliedConfigurations_test.go +++ b/snippet-service/service/http/getAppliedConfigurations_test.go @@ -9,6 +9,7 @@ import ( "github.com/plgd-dev/go-coap/v3/message" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/snippet-service/pb" snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" "github.com/plgd-dev/hub/v2/snippet-service/test" @@ -24,7 +25,7 @@ func TestRequestHandlerGetAppliedConfigurations(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth) defer shutDown() snippetCfg := test.MakeConfig(t) @@ -151,7 +152,7 @@ func TestRequestHandlerGetAppliedConfigurations(t *testing.T) { receivedConfs := make(map[string]*pb.AppliedConfiguration) for { var value pb.AppliedConfiguration - err := httpTest.Unmarshal(resp.StatusCode, resp.Body, &value) + err := pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &value) if errors.Is(err, io.EOF) { break } diff --git a/snippet-service/service/http/getConditions_test.go b/snippet-service/service/http/getConditions_test.go index ad6b07ea3..425781a74 100644 --- a/snippet-service/service/http/getConditions_test.go +++ b/snippet-service/service/http/getConditions_test.go @@ -10,6 +10,7 @@ import ( "github.com/plgd-dev/go-coap/v3/message" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/snippet-service/pb" snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" "github.com/plgd-dev/hub/v2/snippet-service/test" @@ -28,7 +29,7 @@ func TestRequestHandlerGetConditions(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth) defer shutDown() snippetCfg := test.MakeConfig(t) @@ -141,7 +142,7 @@ func TestRequestHandlerGetConditions(t *testing.T) { values := make([]*pb.Condition, 0, 1) for { var value pb.Condition - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &value) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &value) if errors.Is(err, io.EOF) { break } diff --git a/snippet-service/service/http/getConfigurations_test.go b/snippet-service/service/http/getConfigurations_test.go index 6e819cb1c..5663dd416 100644 --- a/snippet-service/service/http/getConfigurations_test.go +++ b/snippet-service/service/http/getConfigurations_test.go @@ -10,6 +10,7 @@ import ( "github.com/plgd-dev/go-coap/v3/message" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/snippet-service/pb" snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" "github.com/plgd-dev/hub/v2/snippet-service/test" @@ -28,7 +29,7 @@ func TestRequestHandlerGetConfigurations(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth) defer shutDown() snippetCfg := test.MakeConfig(t) @@ -146,7 +147,7 @@ func TestRequestHandlerGetConfigurations(t *testing.T) { values := make([]*pb.Configuration, 0, 1) for { var value pb.Configuration - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &value) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &value) if errors.Is(err, io.EOF) { break } diff --git a/snippet-service/service/http/invokeConfiguration_test.go b/snippet-service/service/http/invokeConfiguration_test.go index b477dc445..0a2f0a4f5 100644 --- a/snippet-service/service/http/invokeConfiguration_test.go +++ b/snippet-service/service/http/invokeConfiguration_test.go @@ -19,6 +19,7 @@ import ( "github.com/plgd-dev/hub/v2/pkg/log" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/snippet-service/pb" snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" @@ -53,7 +54,7 @@ func invokeConfiguration(ctx context.Context, t *testing.T, id, token string, re }() var got pb.InvokeConfigurationResponse - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) return &got, resp.StatusCode, err } diff --git a/snippet-service/service/http/service.go b/snippet-service/service/http/service.go index d9ca71cae..ed154de70 100644 --- a/snippet-service/service/http/service.go +++ b/snippet-service/service/http/service.go @@ -3,7 +3,6 @@ package http import ( "fmt" - "github.com/plgd-dev/hub/v2/http-gateway/uri" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" @@ -25,7 +24,7 @@ func New(serviceName string, config Config, snippetServiceServer *grpcService.Sn HTTPConnection: config.Connection, HTTPServer: config.Server, ServiceName: serviceName, - AuthRules: pkgHttp.NewDefaultAuthorizationRules(uri.API), + AuthRules: pkgHttp.NewDefaultAuthorizationRules(API), FileWatcher: fileWatcher, Logger: logger, TraceProvider: tracerProvider, diff --git a/snippet-service/service/http/updateCondition_test.go b/snippet-service/service/http/updateCondition_test.go index 2febb603f..951c76edb 100644 --- a/snippet-service/service/http/updateCondition_test.go +++ b/snippet-service/service/http/updateCondition_test.go @@ -12,6 +12,7 @@ import ( "github.com/plgd-dev/hub/v2/grpc-gateway/pb" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" snippetPb "github.com/plgd-dev/hub/v2/snippet-service/pb" snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" snippetTest "github.com/plgd-dev/hub/v2/snippet-service/test" @@ -46,7 +47,7 @@ func TestRequestHandlerUpdateCondition(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth) defer shutDown() _, shutdownHttp := snippetTest.SetUp(t) @@ -177,7 +178,7 @@ func TestRequestHandlerUpdateCondition(t *testing.T) { require.Equal(t, tt.wantHTTPCode, resp.StatusCode) var got snippetPb.Condition - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) if tt.wantErr { require.Error(t, err) return diff --git a/snippet-service/service/http/updateConfiguration_test.go b/snippet-service/service/http/updateConfiguration_test.go index b44d5a84e..5d6e37683 100644 --- a/snippet-service/service/http/updateConfiguration_test.go +++ b/snippet-service/service/http/updateConfiguration_test.go @@ -12,6 +12,7 @@ import ( "github.com/plgd-dev/hub/v2/grpc-gateway/pb" pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" snippetPb "github.com/plgd-dev/hub/v2/snippet-service/pb" snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" snippetTest "github.com/plgd-dev/hub/v2/snippet-service/test" @@ -30,7 +31,7 @@ func TestRequestHandlerUpdateConfiguration(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) defer cancel() - shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth) defer shutDown() _, shutdownHttp := snippetTest.SetUp(t) @@ -178,7 +179,7 @@ func TestRequestHandlerUpdateConfiguration(t *testing.T) { require.Equal(t, tt.wantHTTPCode, resp.StatusCode) var got snippetPb.Configuration - err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + err = pkgHttpPb.Unmarshal(resp.StatusCode, resp.Body, &got) if tt.wantErr { require.Error(t, err) return diff --git a/snippet-service/service/service_test.go b/snippet-service/service/service_test.go index be6e30b0f..f10f2b011 100644 --- a/snippet-service/service/service_test.go +++ b/snippet-service/service/service_test.go @@ -49,8 +49,7 @@ func TestServiceNew(t *testing.T) { fileWatcher, err := fsnotify.NewWatcher(logger) require.NoError(t, err) - const services = hubTestService.SetUpServicesOAuth - tearDown := hubTestService.SetUpServices(ctx, t, services) + tearDown := hubTestService.SetUpServices(ctx, t, hubTestService.SetUpServicesOAuth|hubTestService.SetUpServicesMachine2MachineOAuth) defer tearDown() tests := []struct { diff --git a/snippet-service/store/appliedConfiguration.go b/snippet-service/store/appliedConfiguration.go index 9664e97e0..a4a87b93f 100644 --- a/snippet-service/store/appliedConfiguration.go +++ b/snippet-service/store/appliedConfiguration.go @@ -44,14 +44,23 @@ func (c *AppliedConfiguration) GetAppliedConfiguration() *pb.AppliedConfiguratio } func (c *AppliedConfiguration) UnmarshalBSON(data []byte) error { - update := func(json map[string]interface{}) { - recordID, ok := json[pb.RecordIDKey] + var recordID string + update := func(json map[string]interface{}) error { + recordIDI, ok := json[pb.RecordIDKey] if ok { - c.RecordID = recordID.(primitive.ObjectID).Hex() + recordID = recordIDI.(primitive.ObjectID).Hex() } delete(json, pb.RecordIDKey) + return nil + } + err := pkgMongo.UnmarshalProtoBSON(data, &c.AppliedConfiguration, update) + if err != nil { + return err } - return pkgMongo.UnmarshalProtoBSON(data, &c.AppliedConfiguration, update) + if c.GetId() == "" && recordID != "" { + c.RecordID = recordID + } + return nil } type UpdateAppliedConfigurationResourceRequest struct { diff --git a/test/config/config.go b/test/config/config.go index 9e87f2871..32d8a82ee 100644 --- a/test/config/config.go +++ b/test/config/config.go @@ -11,6 +11,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/plgd-dev/device/v2/schema" c2curi "github.com/plgd-dev/hub/v2/cloud2cloud-connector/uri" + m2mOauthUri "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" "github.com/plgd-dev/hub/v2/pkg/config/database" "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" pkgCqldb "github.com/plgd-dev/hub/v2/pkg/cqldb" @@ -45,6 +46,7 @@ const ( CERTIFICATE_AUTHORITY_HOST = "localhost:20011" CERTIFICATE_AUTHORITY_HTTP_HOST = "localhost:20012" M2M_OAUTH_SERVER_HTTP_HOST = "localhost:20013" + M2M_OAUTH_SERVER_HOST = "localhost:20016" SNIPPET_SERVICE_HOST = "localhost:20014" SNIPPET_SERVICE_HTTP_HOST = "localhost:20015" GRPC_GW_HOST = "localhost:20005" @@ -61,6 +63,7 @@ const ( OPENTELEMETRY_COLLECTOR_HOST = "localhost:55690" TRUE_STRING = "true" M2M_OAUTH_PRIVATE_KEY_CLIENT_ID = "JWTPrivateKeyClient" + VALIDATOR_CACHE_EXPIRATION = time.Second * 10 ) var ( @@ -298,6 +301,14 @@ func MakeValidatorConfig() validator.Config { Authority: http.HTTPS_SCHEME + OAUTH_SERVER_HOST, HTTP: MakeHttpClientConfig(), }, + { + Authority: http.HTTPS_SCHEME + M2M_OAUTH_SERVER_HTTP_HOST + m2mOauthUri.Base, + HTTP: MakeHttpClientConfig(), + }, + }, + TokenVerification: validator.TokenTrustVerificationConfig{ + Enabled: true, + CacheExpiration: VALIDATOR_CACHE_EXPIRATION, }, } } diff --git a/test/helm/mock.plgd.cloud.yaml b/test/helm/mock.plgd.cloud.yaml index f6a500dbe..971934586 100644 --- a/test/helm/mock.plgd.cloud.yaml +++ b/test/helm/mock.plgd.cloud.yaml @@ -191,22 +191,4 @@ m2moauthserver: enabled: true authorization: audience: - endpoints: - - id: "service" - secretFile: "{{ include \"plgd-hub.m2moauthserver.getClientServiceSecretFile\" . }}" - accessTokenLifetime: 0s - allowedGrantTypes: - - client_credentials - allowedAudiences: [] - allowedScopes: [] - insertTokenClaims: - x-deviceid: "device1" - x-deviceowner: "owner1" - x-array: - - key: "key1" - value: "value1" - - key: "key2" - value: "value2" - x-map: - key1: "value1" - key2: "value2" \ No newline at end of file + endpoints: \ No newline at end of file diff --git a/test/http/unmarshal.go b/test/http/unmarshal.go index c0ba092b9..103a1a8f8 100644 --- a/test/http/unmarshal.go +++ b/test/http/unmarshal.go @@ -2,74 +2,12 @@ package http import ( "encoding/json" - "fmt" "io" "net/http" - jsoniter "github.com/json-iterator/go" - "google.golang.org/genproto/googleapis/rpc/status" - grpcStatus "google.golang.org/grpc/status" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/reflect/protoreflect" + pkgHttpPb "github.com/plgd-dev/hub/v2/pkg/net/http/pb" ) -type Decoder = interface { - Decode(interface{}) error -} - -func UnmarshalError(data []byte) error { - var s status.Status - err := protojson.Unmarshal(data, &s) - if err != nil { - return err - } - return grpcStatus.ErrorProto(&s) -} - -func Unmarshal(code int, input io.Reader, v protoreflect.ProtoMessage) error { - var data json.RawMessage - err := json.NewDecoder(input).Decode(&data) - if err != nil { - return err - } - fmt.Printf("data: %s\n", data) - - if code != http.StatusOK { - return UnmarshalError(data) - } - - var item struct { - Result json.RawMessage `json:"result"` - Error json.RawMessage `json:"error"` - } - - err = jsoniter.Unmarshal(data, &item) - if err != nil { - return err - } - if len(item.Result) == 0 && len(item.Error) == 0 { - u := protojson.UnmarshalOptions{ - DiscardUnknown: true, - } - err = u.Unmarshal(data, v) - if err != nil { - return err - } - return nil - } - if len(item.Error) > 0 { - return UnmarshalError(item.Error) - } - u := protojson.UnmarshalOptions{ - DiscardUnknown: true, - } - err = u.Unmarshal(item.Result, v) - if err != nil { - return err - } - return nil -} - func UnmarshalJson(code int, input io.Reader, v any) error { var data json.RawMessage err := json.NewDecoder(input).Decode(&data) @@ -77,7 +15,7 @@ func UnmarshalJson(code int, input io.Reader, v any) error { return err } if code != http.StatusOK { - return UnmarshalError(data) + return pkgHttpPb.UnmarshalError(data) } err = json.Unmarshal(data, v) return err diff --git a/test/iotivity-lite/service/offboard_test.go b/test/iotivity-lite/service/offboard_test.go index f8c09c2df..b93c654b2 100644 --- a/test/iotivity-lite/service/offboard_test.go +++ b/test/iotivity-lite/service/offboard_test.go @@ -35,7 +35,7 @@ func TestOffboard(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() - const services = service.SetUpServicesOAuth | service.SetUpServicesGrpcGateway + const services = service.SetUpServicesOAuth | service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesGrpcGateway tearDown := service.SetUpServices(ctx, t, services) defer tearDown() @@ -188,7 +188,7 @@ func TestOffboardWithoutSignIn(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() - const services = service.SetUpServicesOAuth | service.SetUpServicesGrpcGateway + const services = service.SetUpServicesOAuth | service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesGrpcGateway tearDown := service.SetUpServices(ctx, t, services) defer tearDown() @@ -251,7 +251,7 @@ func TestOffboardWithSignIn(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() - const services = service.SetUpServicesOAuth | service.SetUpServicesGrpcGateway + const services = service.SetUpServicesOAuth | service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesGrpcGateway tearDown := service.SetUpServices(ctx, t, services) defer tearDown() @@ -316,7 +316,7 @@ func TestOffboardWithSignInByRefreshToken(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() - const services = service.SetUpServicesOAuth | service.SetUpServicesGrpcGateway + const services = service.SetUpServicesOAuth | service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesGrpcGateway tearDown := service.SetUpServices(ctx, t, services) defer tearDown() @@ -393,7 +393,7 @@ func TestOffboardWithRepeat(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() - const services = service.SetUpServicesOAuth | service.SetUpServicesGrpcGateway + const services = service.SetUpServicesOAuth | service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesGrpcGateway tearDown := service.SetUpServices(ctx, t, services) defer tearDown() @@ -455,7 +455,7 @@ func TestOffboardInterrupt(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() - const services = service.SetUpServicesOAuth | service.SetUpServicesGrpcGateway + const services = service.SetUpServicesOAuth | service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesGrpcGateway tearDown := service.SetUpServices(ctx, t, services) defer tearDown() diff --git a/test/iotivity-lite/service/republish_test.go b/test/iotivity-lite/service/republish_test.go index a6367aae6..343a1ac14 100644 --- a/test/iotivity-lite/service/republish_test.go +++ b/test/iotivity-lite/service/republish_test.go @@ -30,7 +30,7 @@ func TestRepublishAfterRefresh(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() - const services = service.SetUpServicesOAuth | service.SetUpServicesGrpcGateway + const services = service.SetUpServicesOAuth | service.SetUpServicesMachine2MachineOAuth | service.SetUpServicesGrpcGateway tearDown := service.SetUpServices(ctx, t, services) defer tearDown() diff --git a/test/oauth-server/service/httpApi.go b/test/oauth-server/service/httpApi.go index 22c0b0956..11e299b89 100644 --- a/test/oauth-server/service/httpApi.go +++ b/test/oauth-server/service/httpApi.go @@ -12,7 +12,7 @@ import ( "github.com/plgd-dev/go-coap/v3/pkg/cache" "github.com/plgd-dev/go-coap/v3/pkg/runner/periodic" "github.com/plgd-dev/hub/v2/pkg/log" - kitHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" pkgJwt "github.com/plgd-dev/hub/v2/pkg/security/jwt" "github.com/plgd-dev/hub/v2/test/oauth-server/uri" ) @@ -65,7 +65,7 @@ func NewRequestHandler(ctx context.Context, config *Config, idTokenKey *rsa.Priv // NewHTTP returns HTTP handler func NewHTTP(requestHandler *RequestHandler, logger log.Logger) http.Handler { r := router.NewRouter() - r.Use(kitHttp.CreateLoggingMiddleware(kitHttp.WithLogger(logger))) + r.Use(pkgHttp.CreateLoggingMiddleware(pkgHttp.WithLogger(logger))) r.StrictSlash(true) // get JWKs diff --git a/test/oauth-server/test/test.go b/test/oauth-server/test/test.go index e91be0e8c..4e172037b 100644 --- a/test/oauth-server/test/test.go +++ b/test/oauth-server/test/test.go @@ -246,7 +246,7 @@ func GetJWTValidator(jwkURL string) *jwt.Validator { Transport: t, Timeout: time.Second * 10, } - return jwt.NewValidator(jwt.NewKeyCache(jwkURL, &client)) + return jwt.NewValidator(jwt.NewKeyCache(jwkURL, &client), log.Get()) } func GetAuthorizationCode(t require.TestingT, authServerHost, clientID, deviceID, scopes string) string { diff --git a/test/pb/pendingCommand.go b/test/pb/pendingCommand.go index 2a7865b5f..80ad16043 100644 --- a/test/pb/pendingCommand.go +++ b/test/pb/pendingCommand.go @@ -10,18 +10,13 @@ import ( "time" "github.com/plgd-dev/go-coap/v3/message" - caService "github.com/plgd-dev/hub/v2/certificate-authority/test" coapgwTest "github.com/plgd-dev/hub/v2/coap-gateway/test" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" - grpcgwService "github.com/plgd-dev/hub/v2/grpc-gateway/test" httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" - idService "github.com/plgd-dev/hub/v2/identity-store/test" "github.com/plgd-dev/hub/v2/pkg/fn" kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" "github.com/plgd-dev/hub/v2/resource-aggregate/events" - raService "github.com/plgd-dev/hub/v2/resource-aggregate/test" - rdService "github.com/plgd-dev/hub/v2/resource-directory/test" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" @@ -71,21 +66,25 @@ func InitPendingEvents(ctx context.Context, t *testing.T) (pb.GrpcGatewayClient, service.ClearDB(ctx, t) - oauthShutdown := oauthTest.SetUp(t) - idShutdown := idService.SetUp(t) - raShutdown := raService.SetUp(t) - rdShutdown := rdService.SetUp(t) - grpcShutdown := grpcgwService.SetUp(t) - caShutdown := caService.SetUp(t) - secureGWShutdown := coapgwTest.SetUp(t) - var closeFunc fn.FuncList - closeFunc.AddFunc(caShutdown) - closeFunc.AddFunc(grpcShutdown) - closeFunc.AddFunc(rdShutdown) - closeFunc.AddFunc(raShutdown) - closeFunc.AddFunc(idShutdown) - closeFunc.AddFunc(oauthShutdown) + deferedClose := true + defer func() { + if deferedClose { + closeFunc.Execute() + } + }() + + tearDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth|service.SetUpServicesMachine2MachineOAuth|service.SetUpServicesId|service.SetUpServicesResourceAggregate| + service.SetUpServicesResourceDirectory|service.SetUpServicesCertificateAuthority|service.SetUpServicesGrpcGateway) + closeFunc.AddFunc(tearDown) + + deferedsecureGWShutdown := true + secureGWShutdown := coapgwTest.SetUp(t) + closeFunc.AddFunc(func() { + if deferedsecureGWShutdown { + secureGWShutdown() + } + }) shutdownHttp := httpgwTest.SetUp(t) closeFunc.AddFunc(shutdownHttp) @@ -105,6 +104,7 @@ func InitPendingEvents(ctx context.Context, t *testing.T) (pb.GrpcGatewayClient, deviceID, shutdownDevSim := test.OnboardDevSim(ctx, t, c, deviceID, config.ACTIVE_COAP_SCHEME+"://"+config.COAP_GW_HOST, test.GetAllBackendResourceLinks()) closeFunc.AddFunc(shutdownDevSim) + deferedsecureGWShutdown = false secureGWShutdown() createFn := func() { @@ -208,6 +208,7 @@ func InitPendingEvents(ctx context.Context, t *testing.T) (pb.GrpcGatewayClient, } } + deferedClose = false return c, resourcePendings, devicePendings, closeFunc.ToFunction() } diff --git a/test/service/service.go b/test/service/service.go index c3665a753..2dee6e893 100644 --- a/test/service/service.go +++ b/test/service/service.go @@ -161,7 +161,7 @@ func WithM2MOAuthConfig(oauth m2mOauthService.Config) SetUpOption { type SetUpOption = func(cfg *Config) -func SetUp(ctx context.Context, t require.TestingT, opts ...SetUpOption) (tearDown func()) { +func SetUp(ctx context.Context, t require.TestingT, opts ...SetUpOption) func() { config := Config{ COAPGW: coapgwTest.MakeConfig(t), RD: rdTest.MakeConfig(t), @@ -177,30 +177,40 @@ func SetUp(ctx context.Context, t require.TestingT, opts ...SetUpOption) (tearDo o(&config) } + var tearDown fn.FuncList + deferedTearDown := true + defer func() { + if deferedTearDown { + tearDown.Execute() + } + }() + ClearDB(ctx, t) oauthShutdown := oauthTest.New(t, config.OAUTH) + tearDown.AddFunc(oauthShutdown) m2mTearDown := m2mOauthTest.New(t, config.M2MOAUTH) + tearDown.AddFunc(m2mTearDown) isShutdown := isTest.New(t, config.IS) + tearDown.AddFunc(isShutdown) raShutdown := raTest.New(t, config.RA) + tearDown.AddFunc(raShutdown) rdShutdown := rdTest.New(t, config.RD) + tearDown.AddFunc(rdShutdown) grpcShutdown := grpcgwTest.New(t, config.GRPCGW) + tearDown.AddFunc(grpcShutdown) c2cgwShutdown := c2cgwService.SetUp(t) + tearDown.AddFunc(c2cgwShutdown) caShutdown := caService.New(t, config.CA) + tearDown.AddFunc(caShutdown) secureGWShutdown := coapgwTest.New(t, config.COAPGW) + tearDown.AddFunc(secureGWShutdown) // wait for all services to start time.Sleep(time.Second) + deferedTearDown = false return func() { - caShutdown() - c2cgwShutdown() - grpcShutdown() - secureGWShutdown() - rdShutdown() - raShutdown() - isShutdown() - m2mTearDown() - oauthShutdown() + tearDown.Execute() // wait for all services to be closed time.Sleep(time.Second) @@ -211,6 +221,7 @@ type SetUpServicesConfig uint16 const ( SetUpServicesOAuth SetUpServicesConfig = 1 << iota + SetUpServicesMachine2MachineOAuth SetUpServicesId SetUpServicesResourceAggregate SetUpServicesResourceDirectory @@ -224,14 +235,29 @@ const ( var setupServicesMap = map[SetUpServicesConfig]func(t require.TestingT, tearDown *fn.FuncList, opts ...SetUpOption){ SetUpServicesOAuth: func(t require.TestingT, tearDown *fn.FuncList, opts ...SetUpOption) { - // to fix `opts` is unused - config := Config{} + config := Config{ + OAUTH: oauthTest.MakeConfig(t), + } for _, o := range opts { o(&config) } - oauthShutdown := oauthTest.SetUp(t) + err := config.OAUTH.Validate() + require.NoError(t, err) + oauthShutdown := oauthTest.New(t, config.OAUTH) tearDown.AddFunc(oauthShutdown) }, + SetUpServicesMachine2MachineOAuth: func(t require.TestingT, tearDown *fn.FuncList, opts ...SetUpOption) { + config := Config{ + M2MOAUTH: m2mOauthTest.MakeConfig(t), + } + for _, o := range opts { + o(&config) + } + err := config.M2MOAUTH.Validate() + require.NoError(t, err) + m2mTearDown := m2mOauthTest.New(t, config.M2MOAUTH) + tearDown.AddFunc(m2mTearDown) + }, SetUpServicesId: func(t require.TestingT, tearDown *fn.FuncList, opts ...SetUpOption) { config := Config{ IS: isTest.MakeConfig(t), @@ -280,12 +306,7 @@ var setupServicesMap = map[SetUpServicesConfig]func(t require.TestingT, tearDown grpcShutdown := grpcgwTest.New(t, config.GRPCGW) tearDown.AddFunc(grpcShutdown) }, - SetUpServicesCloud2CloudGateway: func(t require.TestingT, tearDown *fn.FuncList, opts ...SetUpOption) { - // to fix `opts` is unused - config := Config{} - for _, o := range opts { - o(&config) - } + SetUpServicesCloud2CloudGateway: func(t require.TestingT, tearDown *fn.FuncList, _ ...SetUpOption) { c2cgwShutdown := c2cgwService.SetUp(t) tearDown.AddFunc(c2cgwShutdown) }, @@ -317,6 +338,12 @@ var setupServicesMap = map[SetUpServicesConfig]func(t require.TestingT, tearDown func SetUpServices(ctx context.Context, t require.TestingT, servicesConfig SetUpServicesConfig, opts ...SetUpOption) func() { var tearDown fn.FuncList + deferedTearDown := true + defer func() { + if deferedTearDown { + tearDown.Execute() + } + }() ClearDB(ctx, t) for i := SetUpServicesConfig(1); i < SetUpServicesMax; i <<= 1 { @@ -326,5 +353,6 @@ func SetUpServices(ctx context.Context, t require.TestingT, servicesConfig SetUp } } } + deferedTearDown = false return tearDown.ToFunction() }