From f8b535d5c118599c28e4ef6bdafc5a1f0e95dc86 Mon Sep 17 00:00:00 2001 From: Ivan Josipovic <9521987+IvanJosipovic@users.noreply.github.com> Date: Thu, 29 Sep 2022 19:33:20 -0700 Subject: [PATCH] feat: initial version (#3) --- .github/renovate.json | 16 ++ .github/workflows/cicd.yml | 91 +++++++++ .releaserc.json | 23 +++ README.md | 69 ++++++- charts/ingress-nginx-validate-jwt/.helmignore | 23 +++ charts/ingress-nginx-validate-jwt/Chart.yaml | 24 +++ .../templates/NOTES.txt | 22 +++ .../templates/_helpers.tpl | 62 ++++++ .../templates/deployment.yaml | 66 +++++++ .../templates/hpa.yaml | 28 +++ .../templates/ingress.yaml | 61 ++++++ .../templates/service.yaml | 15 ++ .../templates/serviceaccount.yaml | 12 ++ .../templates/tests/test-connection.yaml | 15 ++ charts/ingress-nginx-validate-jwt/values.yaml | 91 +++++++++ docs/validate-jwt.drawio | 1 + docs/validate-jwt.png | Bin 0 -> 45019 bytes global.json | 7 + ingress-nginx-validate-jwt-tests/UnitTest1.cs | 183 ++++++++++++++++++ ingress-nginx-validate-jwt-tests/Usings.cs | 1 + .../ingress-nginx-validate-jwt-tests.csproj | 31 +++ ingress-nginx-validate-jwt.sln | 38 ++++ .../Controllers/AuthController.cs | 88 +++++++++ .../Controllers/HealthController.cs | 26 +++ ingress-nginx-validate-jwt/HostedService.cs | 28 +++ .../ISettingsService.cs | 9 + ingress-nginx-validate-jwt/Program.cs | 36 ++++ .../Properties/launchSettings.json | 14 ++ ingress-nginx-validate-jwt/SettingsService.cs | 41 ++++ .../appsettings.Development.json | 11 ++ ingress-nginx-validate-jwt/appsettings.json | 10 + .../ingress-nginx-validate-jwt.csproj | 28 +++ 32 files changed, 1169 insertions(+), 1 deletion(-) create mode 100644 .github/renovate.json create mode 100644 .github/workflows/cicd.yml create mode 100644 .releaserc.json create mode 100644 charts/ingress-nginx-validate-jwt/.helmignore create mode 100644 charts/ingress-nginx-validate-jwt/Chart.yaml create mode 100644 charts/ingress-nginx-validate-jwt/templates/NOTES.txt create mode 100644 charts/ingress-nginx-validate-jwt/templates/_helpers.tpl create mode 100644 charts/ingress-nginx-validate-jwt/templates/deployment.yaml create mode 100644 charts/ingress-nginx-validate-jwt/templates/hpa.yaml create mode 100644 charts/ingress-nginx-validate-jwt/templates/ingress.yaml create mode 100644 charts/ingress-nginx-validate-jwt/templates/service.yaml create mode 100644 charts/ingress-nginx-validate-jwt/templates/serviceaccount.yaml create mode 100644 charts/ingress-nginx-validate-jwt/templates/tests/test-connection.yaml create mode 100644 charts/ingress-nginx-validate-jwt/values.yaml create mode 100644 docs/validate-jwt.drawio create mode 100644 docs/validate-jwt.png create mode 100644 global.json create mode 100644 ingress-nginx-validate-jwt-tests/UnitTest1.cs create mode 100644 ingress-nginx-validate-jwt-tests/Usings.cs create mode 100644 ingress-nginx-validate-jwt-tests/ingress-nginx-validate-jwt-tests.csproj create mode 100644 ingress-nginx-validate-jwt.sln create mode 100644 ingress-nginx-validate-jwt/Controllers/AuthController.cs create mode 100644 ingress-nginx-validate-jwt/Controllers/HealthController.cs create mode 100644 ingress-nginx-validate-jwt/HostedService.cs create mode 100644 ingress-nginx-validate-jwt/ISettingsService.cs create mode 100644 ingress-nginx-validate-jwt/Program.cs create mode 100644 ingress-nginx-validate-jwt/Properties/launchSettings.json create mode 100644 ingress-nginx-validate-jwt/SettingsService.cs create mode 100644 ingress-nginx-validate-jwt/appsettings.Development.json create mode 100644 ingress-nginx-validate-jwt/appsettings.json create mode 100644 ingress-nginx-validate-jwt/ingress-nginx-validate-jwt.csproj diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..cb78dad --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,16 @@ +{ + "enabled": true, + "timezone": "America/Vancouver", + "dependencyDashboard": true, + "dependencyDashboardTitle": "Renovate Dashboard", + "commitMessageSuffix": "", + "commitBody": "", + "semanticCommits": "enabled", + "suppressNotifications": ["prIgnoreNotification"], + "rebaseWhen": "conflicted", + "assignees": ["@ivanjosipovic"], + "extends": [ + "config:base" + ] +} + diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 0000000..d5d7a8a --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,91 @@ +name: CICD + +on: + workflow_dispatch: + push: + branches: + - 'main' + - 'alpha' + - 'beta' + - 'dev' + pull_request: + types: [opened, reopened, synchronize] + +env: + semantic_version: 19 + +jobs: + create-release: + name: Create Release + runs-on: ubuntu-latest + outputs: + new_release_published: ${{ steps.semantic.outputs.new_release_published }} + new_release_version: ${{ (steps.semantic.outputs.new_release_published && steps.semantic.outputs.new_release_version) || '0.0.1' }} + steps: + - uses: actions/checkout@v3 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Semantic Release + uses: cycjimmy/semantic-release-action@v2 + id: semantic + with: + semantic_version: ${{ env.semantic_version }} + dry_run: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup .NET Core + uses: actions/setup-dotnet@v2 + with: + global-json-file: global.json + + - name: .NET Build + run: dotnet build -c Release + + - name: .NET Test + run: dotnet test -c Release --collect:"XPlat Code Coverage" + + - name: Coverage + uses: codecov/codecov-action@v3 + with: + file: coverage.cobertura.xml + + - name: Build Image + run: dotnet publish -c Release --os linux --arch x64 -p:PublishProfile=DefaultContainer -p:Version=${{ (steps.semantic.outputs.new_release_published && steps.semantic.outputs.new_release_version) || '0.0.1' }} + + - name: Docker Push + if: steps.semantic.outputs.new_release_published == 'true' + run: | + echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${GITHUB_ACTOR} --password-stdin + docker tag ingress-nginx-validate-jwt:${{ (steps.semantic.outputs.new_release_published && steps.semantic.outputs.new_release_version) }} ghcr.io/${GITHUB_REPOSITORY,,}/ingress-nginx-validate-jwt:${{ (steps.semantic.outputs.new_release_published && steps.semantic.outputs.new_release_version) }} + docker push ghcr.io/${GITHUB_REPOSITORY,,}/ingress-nginx-validate-jwt:${{ (steps.semantic.outputs.new_release_published && steps.semantic.outputs.new_release_version) }} + + - name: Semantic Release + if: steps.semantic.outputs.new_release_published == 'true' + uses: cycjimmy/semantic-release-action@v2 + with: + semantic_version: ${{ env.semantic_version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Helm + if: steps.semantic.outputs.new_release_published == 'true' + uses: azure/setup-helm@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Update Helm Versionin + if: steps.semantic.outputs.new_release_published == 'true' + shell: bash + run: | + sed -i 's/0.0.1/${{ (steps.semantic.outputs.new_release_published && steps.semantic.outputs.new_release_version) || '0.0.1' }}/' ./charts/ingress-nginx-validate-jwt/Chart.yaml + + - name: Run chart-releaser + if: steps.semantic.outputs.new_release_published == 'true' + uses: helm/chart-releaser-action@v1 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..6cab66f --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,23 @@ +{ + "branches": [ + "main", + { + "name": "beta", + "prerelease": true + }, + { + "name": "alpha", + "prerelease": true + } + ], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/github", + { + "assets": [] + } + ] + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 30e209b..01f988d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,69 @@ # ingress-nginx-validate-jwt -ingress-nginx-validate-jwt + +[![codecov](https://codecov.io/gh/IvanJosipovic/ingress-nginx-validate-jwt/branch/main/graph/badge.svg?token=hh1FWYrH5r)](https://codecov.io/gh/IvanJosipovic/ingress-nginx-validate-jwt) + +## What is this? + +This project is an API server which is used along with the [nginx.ingress.kubernetes.io/auth-url](https://github.com/kubernetes/ingress-nginx/blob/main/docs/user-guide/nginx-configuration/annotations.md#external-authentication) annotation for ingress-nginx and enables per Ingress customizable JWT validation. + +## Install + +```bash +helm repo add ingress-nginx-validate-jwt https://ivanjosipovic.github.io/ingress-nginx-validate-jwt + +helm repo update + +helm install ingress-nginx-validate-jwt \ +ingress-nginx-validate-jwt/ingress-nginx-validate-jwt \ +--create-namespace \ +--namespace ingress-nginx-validate-jwt \ +--set openIdProviderConfigurationUrl="https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration" +``` + +### Options + +- openIdProviderConfigurationUrl + - OpenID Provider Configuration Url for your Identity Provider +- logLevel + - Logging Level (Trace, Debug, Information, Warning, Error, Critical, and None) +- [Helm Values](charts/ingress-nginx-validate-jwt/values.yaml) + +## Configure Ingress + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress + namespace: default + annotations: + nginx.ingress.kubernetes.io/auth-url: http://ingress-nginx-validate-jwt.ingress-nginx-validate-jwt.svc.cluster.local/auth?tid=11111111-1111-1111-1111-111111111111&aud=22222222-2222-2222-2222-222222222222&aud=33333333-3333-3333-3333-333333333333 +spec: +``` + +## Parameters + +The /auth endpoint supports configurable parameters in the format of {claim}={value}. In the case the same claim is called more than once, the traffic will have to match only one. + +For example, using the following query string +/auth? +tid=11111111-1111-1111-1111-111111111111 +&aud=22222222-2222-2222-2222-222222222222 +&aud=33333333-3333-3333-3333-333333333333 + +Along with validating the JWT token, the token must have a claim tid=11111111-1111-1111-1111-111111111111 and one of aud=22222222-2222-2222-2222-222222222222 + or aud=33333333-3333-3333-3333-333333333333 + +## Design + +![alt text](/docs/validate-jwt.png) + +## Metrics + +Metrics are exposed on :80/metrics + +| Metric Name | Description | +|---|---| +| ingress_nginx_validate_jwt_authorized | Number of Authorized operations ongoing | +| ingress_nginx_validate_jwt_unauthorized | Number of Unauthorized operations ongoing | +| ingress_nginx_validate_jwt_duration_seconds | Histogram of JWT validation durations | diff --git a/charts/ingress-nginx-validate-jwt/.helmignore b/charts/ingress-nginx-validate-jwt/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/ingress-nginx-validate-jwt/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/ingress-nginx-validate-jwt/Chart.yaml b/charts/ingress-nginx-validate-jwt/Chart.yaml new file mode 100644 index 0000000..a31cd35 --- /dev/null +++ b/charts/ingress-nginx-validate-jwt/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: ingress-nginx-validate-jwt +description: Enables ingress-nginx to validate JWT tokens + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.0.1 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.0.1" diff --git a/charts/ingress-nginx-validate-jwt/templates/NOTES.txt b/charts/ingress-nginx-validate-jwt/templates/NOTES.txt new file mode 100644 index 0000000..e9403f9 --- /dev/null +++ b/charts/ingress-nginx-validate-jwt/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ingress-nginx-validate-jwt.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ingress-nginx-validate-jwt.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ingress-nginx-validate-jwt.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ingress-nginx-validate-jwt.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/charts/ingress-nginx-validate-jwt/templates/_helpers.tpl b/charts/ingress-nginx-validate-jwt/templates/_helpers.tpl new file mode 100644 index 0000000..4c9aee1 --- /dev/null +++ b/charts/ingress-nginx-validate-jwt/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ingress-nginx-validate-jwt.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ingress-nginx-validate-jwt.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ingress-nginx-validate-jwt.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ingress-nginx-validate-jwt.labels" -}} +helm.sh/chart: {{ include "ingress-nginx-validate-jwt.chart" . }} +{{ include "ingress-nginx-validate-jwt.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ingress-nginx-validate-jwt.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ingress-nginx-validate-jwt.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ingress-nginx-validate-jwt.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ingress-nginx-validate-jwt.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/ingress-nginx-validate-jwt/templates/deployment.yaml b/charts/ingress-nginx-validate-jwt/templates/deployment.yaml new file mode 100644 index 0000000..e894570 --- /dev/null +++ b/charts/ingress-nginx-validate-jwt/templates/deployment.yaml @@ -0,0 +1,66 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ingress-nginx-validate-jwt.fullname" . }} + labels: + {{- include "ingress-nginx-validate-jwt.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "ingress-nginx-validate-jwt.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "ingress-nginx-validate-jwt.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ingress-nginx-validate-jwt.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + env: + - name: "OpenIdProviderConfigurationUrl" + value: "{{ .Values.openIdProviderConfigurationUrl }}" + - name: "Logging__LogLevel__Default" + value: "{{ .Values.logLevel }}" + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: /health + port: http + readinessProbe: + httpGet: + path: /health + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/ingress-nginx-validate-jwt/templates/hpa.yaml b/charts/ingress-nginx-validate-jwt/templates/hpa.yaml new file mode 100644 index 0000000..c048f2c --- /dev/null +++ b/charts/ingress-nginx-validate-jwt/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ingress-nginx-validate-jwt.fullname" . }} + labels: + {{- include "ingress-nginx-validate-jwt.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ingress-nginx-validate-jwt.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/ingress-nginx-validate-jwt/templates/ingress.yaml b/charts/ingress-nginx-validate-jwt/templates/ingress.yaml new file mode 100644 index 0000000..4df034b --- /dev/null +++ b/charts/ingress-nginx-validate-jwt/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ingress-nginx-validate-jwt.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ingress-nginx-validate-jwt.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/ingress-nginx-validate-jwt/templates/service.yaml b/charts/ingress-nginx-validate-jwt/templates/service.yaml new file mode 100644 index 0000000..71c824c --- /dev/null +++ b/charts/ingress-nginx-validate-jwt/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ingress-nginx-validate-jwt.fullname" . }} + labels: + {{- include "ingress-nginx-validate-jwt.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ingress-nginx-validate-jwt.selectorLabels" . | nindent 4 }} diff --git a/charts/ingress-nginx-validate-jwt/templates/serviceaccount.yaml b/charts/ingress-nginx-validate-jwt/templates/serviceaccount.yaml new file mode 100644 index 0000000..43b19f8 --- /dev/null +++ b/charts/ingress-nginx-validate-jwt/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ingress-nginx-validate-jwt.serviceAccountName" . }} + labels: + {{- include "ingress-nginx-validate-jwt.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/ingress-nginx-validate-jwt/templates/tests/test-connection.yaml b/charts/ingress-nginx-validate-jwt/templates/tests/test-connection.yaml new file mode 100644 index 0000000..54510ab --- /dev/null +++ b/charts/ingress-nginx-validate-jwt/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "ingress-nginx-validate-jwt.fullname" . }}-test-connection" + labels: + {{- include "ingress-nginx-validate-jwt.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "ingress-nginx-validate-jwt.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/charts/ingress-nginx-validate-jwt/values.yaml b/charts/ingress-nginx-validate-jwt/values.yaml new file mode 100644 index 0000000..1a0d610 --- /dev/null +++ b/charts/ingress-nginx-validate-jwt/values.yaml @@ -0,0 +1,91 @@ +# Default values for ingress-nginx-validate-jwt. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: ghcr.io/ivanjosipovic/ingress-nginx-validate-jwt/ingress-nginx-validate-jwt + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +# OpenID Provider Configuration Url for your Identity Provider +openIdProviderConfigurationUrl: "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration" + +# Log Level +logLevel: Information + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: + prometheus.io/scrape: 'true' + prometheus.io/port: '80' + prometheus.io/path: '/metrics' + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/docs/validate-jwt.drawio b/docs/validate-jwt.drawio new file mode 100644 index 0000000..6ccb091 --- /dev/null +++ b/docs/validate-jwt.drawio @@ -0,0 +1 @@ +7Vpdd5s2GP41vrQPCOPgy9hJ1qztmjU563q1I4OM1ciIChE7+fV7JYT5EM681g45PfFFgh4JSTzvt2Dgzdfb3wROVx95RNgAOdF24F0MEJoGLvxVwGMB+FMDxIJGBVQDbukTMaBj0JxGJGsMlJwzSdMmGPIkIaFsYFgIvmkOW3LWXDXFMbGA2xAzG/1CI7kq0MB3KvwdofGqXNl1TM8al4MNkK1wxDc1yLsceHPBuSyu1ts5YYq7kpfivqs9vbuNCZLIQ27wrmLx9D74+lcgFnfe54/ZBv85NLM8YJabBzablY8lA4LnSUTUJM7Am21WVJLbFIeqdwMiB2wl1wxaLlya6YiQZLt3n+7u6UFrCF8TKR5hiLlhbPgyCoM8095U9HsTg61q1KMSxEbk8W7qihW4MMT8D5KQRdL7fEFEQiRoZpsunKWFHi7pVrFWZyflNJF6d/5s4F8AghmNEwBCIIcIAOhaK+RsyRNprMFFFX5B1zE8A6ML+IufckHUs4HyS0wTImA7V9Xe/rkl4oGGJBtlD/FxhOO3hDO2hTP2R55ni2d8MulMLBmQCGzYNLmQKx7zBLPLCp01lboa84Hz1AjrG5Hy0YgA55I3RankM+eMC72i5+gf4JnEQp4rx6OkynCW0bCErygrb8+k4PekNkGAFt5koiaGQTU88kkQjXfCU0/2vOiACJ6LkDzDmJEObCkm8plxk25VEIRhSR+a+zi+WM9+BbFenk9mWqw9iM8N+pSfZznNJKbJdkiTWJDM9pvZPZHhykiuJpEIZ6tm9Mma0adlMCg48y9RlyyW+me54Zkzchx/gObOaOKpf8ozo7nucDWMWui0E9VTtEdO90x8pu+G/o5J3BaGgtZYHTkgnUgVC+ttrBKv0f3O7Y8oxAP1mOJaX1ykXMUh5fcpJDbnJuRIZRIzhheE3fCMSsobgagc/qE1YMGl5Ouu0KWnmuHwPtZmWDIfkSXOmTxO/LGyA2QHIL8jORgHp3JT/utyU/v8z4EO7Ihuanygm/L69FJjy0udhxLIs7yTsbZ8zYoBBxhIt8HVpMhzySBrm+9KF+cQKyoGcBERUXYmPCFHsq9WgteVfHeY18lS74klH7ikEZZk+G0j34LILxdEThAjvGnPMWJqKTFMM9rAncP7hG8SaPGUJDQaAtVLGucCa556CCxAsXj8W92v1c4AXzUA+mfaF1uzQtF6rLduiKBAmxKmBo8YUYKfq1uMegzBIv0zr6EiwzKNODjqmOlvlBeoDeHLZUakpTG7Xfy4EgWWEl1HwCCVsJ2JDgwLCFuTWF3dCP5AIT7YGsQYTTNSs/yQ8Tx6kXMd3/WbZhnYZumiDrsMThVcXPtgx32GstNTtIuthiLXtynqYgidjCG7irMPCPtkyCtPf3pjyM4gvX4ZCl4bQ3YON+6VoZ2TKWvIoG+GbN/++5c7AMCPS6gMIKAj5/zm2mLtLd19S3dBnc9GHppWv6Bp/31nv8iOIR1nJi9p/2evzP6RHUM63mn0GEN6z0OQ7SGdXhny3P+uMV+WIbvIBOd/DsgnKCSdW3XOl6fKPSKHcawiyieoOeHfNYx1WoWnsxTgr6APSgxdYThVSVEVGzXyJ99z9R5bH0wOM11dqrVdP91qwsv+skJxi35z2Oas8b16leq8u7u7gX+fyfecZFLTCWwipwiHdxCbkiNtABX9f6h3EupBy7cSzpKLDRZR1l5VdRT8quMnzdJRNuIV/c1jrV1TbUOaDeBESS1kmK6zIy0+3seCfqMcSr38iuy4sBKSo+zCL/r3LAE1eZbyBAz7OKtNms+8ex8F1p9lmvBqQeC+0tKWuwHnIZt+pZk9mUPZeqploDILYGQp9yYZXT6sebzzEyfF+94f1L868I/kKVsnyl1fDHR9zuH+wAcD0Ky+pykOXqqPkrzLfwE= \ No newline at end of file diff --git a/docs/validate-jwt.png b/docs/validate-jwt.png new file mode 100644 index 0000000000000000000000000000000000000000..52f3ac06f7cbb79db24541b2dc25254c6b54b313 GIT binary patch literal 45019 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 XV43({;ou#&1MsJWRhKWmiVOJ claims) + { + return "Bearer " + s_tokenHandler.WriteToken(new JwtSecurityToken(Issuer, null, claims, null, DateTime.UtcNow.AddMinutes(20), SigningCredentials)); + } + } + + public static IEnumerable GetTests() + { + return new List + { + new object[] + { + "", + new List + { + }, + typeof(OkResult) + }, + new object[] + { + "", + new List + { + }, + typeof(UnauthorizedResult), + true + }, + new object[] + { + "?tid=11111111-1111-1111-1111-111111111111", + new List + { + new Claim("tid", "11111111-1111-1111-1111-111111111111") + }, + typeof(OkResult) + }, + new object[] + { + "?tid=11111111-1111-1111-1111-111111111111&aud=22222222-2222-2222-2222-222222222222", + new List + { + new Claim("tid", "11111111-1111-1111-1111-111111111111"), + new Claim("aud", "22222222-2222-2222-2222-222222222222"), + }, + typeof(OkResult) + }, + new object[] + { + "?tid=11111111-1111-1111-1111-111111111111&aud=22222222-2222-2222-2222-222222222222&aud=33333333-3333-3333-3333-333333333333", + new List + { + new Claim("tid", "11111111-1111-1111-1111-111111111111"), + new Claim("aud", "33333333-3333-3333-3333-333333333333") + }, + typeof(OkResult) + }, + new object[] + { + "?tid=11111111-1111-1111-1111-111111111111&aud=22222222-2222-2222-2222-222222222222&aud=33333333-3333-3333-3333-333333333333", + new List + { + new Claim("tid", "11111111-1111-1111-1111-111111111111"), + new Claim("aud", "22222222-2222-2222-2222-222222222222"), + new Claim("aud", "33333333-3333-3333-3333-333333333333") + }, + typeof(OkResult) + }, + + + new object[] + { + "?tid=11111111-1111-1111-1111-111111111111", + new List + { + }, + typeof(UnauthorizedResult) + }, + + new object[] + { + "?tid=11111111-1111-1111-1111-111111111111", + new List + { + new Claim("tid", "22222222-2222-2222-2222-222222222222") + }, + typeof(UnauthorizedResult) + }, + + new object[] + { + "?tid=11111111-1111-1111-1111-111111111111&aud=22222222-2222-2222-2222-222222222222&aud=33333333-3333-3333-3333-333333333333", + new List + { + }, + typeof(UnauthorizedResult) + }, + + new object[] + { + "?tid=11111111-1111-1111-1111-111111111111&aud=22222222-2222-2222-2222-222222222222&aud=33333333-3333-3333-3333-333333333333", + new List + { + new Claim("tid", "") + }, + typeof(UnauthorizedResult) + }, + }; + } + + [Theory] + [MemberData(nameof(GetTests))] + public async Task Test1(string query, List claims, Type type, bool nullAuth = false) + { + IdentityModelEventSource.ShowPII = true; + + var settingsService = new Mock(); + var config = new OpenIdConnectConfiguration() + { + Issuer = MockJwtTokens.Issuer + }; + config.SigningKeys.Add(MockJwtTokens.SecurityKey); + + settingsService.Setup(x => x.GetConfiguration(new CancellationToken())).Returns(Task.FromResult(config)); + + var httpContext = new DefaultHttpContext(); + if (!nullAuth) + { + httpContext.Request.Headers.Authorization = MockJwtTokens.GenerateJwtToken(claims); + } + httpContext.Request.QueryString = new QueryString(query); + + var controllerContext = new ControllerContext() + { + HttpContext = httpContext, + }; + + var controller = new AuthController(new Mock>().Object, settingsService.Object, new JwtSecurityTokenHandler()) + { + ControllerContext = controllerContext, + }; + + var result = await controller.Get(new CancellationToken()); + + result.Should().BeOfType(type); + } + } +} \ No newline at end of file diff --git a/ingress-nginx-validate-jwt-tests/Usings.cs b/ingress-nginx-validate-jwt-tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/ingress-nginx-validate-jwt-tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/ingress-nginx-validate-jwt-tests/ingress-nginx-validate-jwt-tests.csproj b/ingress-nginx-validate-jwt-tests/ingress-nginx-validate-jwt-tests.csproj new file mode 100644 index 0000000..dfaba6d --- /dev/null +++ b/ingress-nginx-validate-jwt-tests/ingress-nginx-validate-jwt-tests.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + ingress_nginx_validate_jwt_tests + enable + enable + + false + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/ingress-nginx-validate-jwt.sln b/ingress-nginx-validate-jwt.sln new file mode 100644 index 0000000..41963ed --- /dev/null +++ b/ingress-nginx-validate-jwt.sln @@ -0,0 +1,38 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.32916.344 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ingress-nginx-validate-jwt", "ingress-nginx-validate-jwt\ingress-nginx-validate-jwt.csproj", "{F699F52C-DBCD-4BFD-807A-0D74DB3D7253}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ingress-nginx-validate-jwt-tests", "ingress-nginx-validate-jwt-tests\ingress-nginx-validate-jwt-tests.csproj", "{587A7638-97C0-4DDC-9297-745ED5816CA4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B7D3A0E7-2264-4E9A-90FC-23B8F0CDE196}" + ProjectSection(SolutionItems) = preProject + .github\workflows\cicd.yml = .github\workflows\cicd.yml + README.md = README.md + .github\renovate.json = .github\renovate.json + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F699F52C-DBCD-4BFD-807A-0D74DB3D7253}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F699F52C-DBCD-4BFD-807A-0D74DB3D7253}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F699F52C-DBCD-4BFD-807A-0D74DB3D7253}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F699F52C-DBCD-4BFD-807A-0D74DB3D7253}.Release|Any CPU.Build.0 = Release|Any CPU + {587A7638-97C0-4DDC-9297-745ED5816CA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {587A7638-97C0-4DDC-9297-745ED5816CA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {587A7638-97C0-4DDC-9297-745ED5816CA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {587A7638-97C0-4DDC-9297-745ED5816CA4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {63820EBD-4F2D-40A4-81B1-CB53D93782C2} + EndGlobalSection +EndGlobal diff --git a/ingress-nginx-validate-jwt/Controllers/AuthController.cs b/ingress-nginx-validate-jwt/Controllers/AuthController.cs new file mode 100644 index 0000000..673f436 --- /dev/null +++ b/ingress-nginx-validate-jwt/Controllers/AuthController.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using Prometheus; +using System.IdentityModel.Tokens.Jwt; + +namespace ingress_nginx_validate_jwt.Controllers; + +[ApiController] +[Route("[controller]")] +public class AuthController : ControllerBase +{ + private readonly ILogger _logger; + + private ISettingsService _settingsService; + + private JwtSecurityTokenHandler _jwtSecurityTokenHandler; + + private static readonly Gauge Authorized = Metrics.CreateGauge("ingress_nginx_validate_jwt_authorized", "Number of Authorized operations ongoing."); + + private static readonly Gauge Unauthorized = Metrics.CreateGauge("ingress_nginx_validate_jwt_unauthorized", "Number of Unauthorized operations ongoing."); + + private static readonly Histogram ValidationDuration = Metrics.CreateHistogram("ingress_nginx_validate_jwt_duration_seconds", "Histogram of JWT validation durations."); + + public AuthController(ILogger logger, ISettingsService settingsService, JwtSecurityTokenHandler jwtSecurityTokenHandler) + { + _logger = logger; + _settingsService = settingsService; + _jwtSecurityTokenHandler = jwtSecurityTokenHandler; + } + + [HttpGet] + public async Task Get(CancellationToken cancellationToken) + { + using (ValidationDuration.NewTimer()) + { + try + { + var token = Request.Headers.Authorization.FirstOrDefault(); + + if (string.IsNullOrEmpty(token)) + { + Unauthorized.Inc(); + return Unauthorized(); + } + + // Remove "Bearer " + if (token.StartsWith("Bearer ")) + { + token = token.Substring(7); + } + + var settings = await _settingsService.GetConfiguration(cancellationToken); + + var parameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKeys = settings.SigningKeys, + ValidateIssuer = false, + ValidateAudience = false, + ClockSkew = TimeSpan.FromSeconds(0) + }; + + _jwtSecurityTokenHandler.ValidateToken(token, parameters, out SecurityToken validatedToken); + + var jwtToken = (JwtSecurityToken)validatedToken; + + foreach (var item in Request.Query) + { + var claim = jwtToken.Claims.First(x => x.Type == item.Key).Value; + + if (!item.Value.Contains(claim)) + { + Unauthorized.Inc(); + return Unauthorized(); + } + } + + Authorized.Inc(); + return Ok(); + } + catch + { + Unauthorized.Inc(); + return Unauthorized(); + } + } + } +} \ No newline at end of file diff --git a/ingress-nginx-validate-jwt/Controllers/HealthController.cs b/ingress-nginx-validate-jwt/Controllers/HealthController.cs new file mode 100644 index 0000000..a9302cb --- /dev/null +++ b/ingress-nginx-validate-jwt/Controllers/HealthController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; + +namespace ingress_nginx_validate_jwt.Controllers; + +[ApiController] +[Route("[controller]")] +public class HealthController : ControllerBase +{ + private readonly ILogger _logger; + + private ISettingsService _settingsService; + + public HealthController(ILogger logger, ISettingsService settingsService) + { + _logger = logger; + _settingsService = settingsService; + } + + [HttpGet] + public async Task Get(CancellationToken cancellationToken) + { + await _settingsService.GetConfiguration(cancellationToken); + + return Ok(); + } +} \ No newline at end of file diff --git a/ingress-nginx-validate-jwt/HostedService.cs b/ingress-nginx-validate-jwt/HostedService.cs new file mode 100644 index 0000000..921bad3 --- /dev/null +++ b/ingress-nginx-validate-jwt/HostedService.cs @@ -0,0 +1,28 @@ +using System.Diagnostics; + +namespace ingress_nginx_validate_jwt; + +public class HostedService : IHostedService +{ + private ILogger _logger; + + private ISettingsService _settingsService; + + public HostedService(ILogger logger, ISettingsService settingsService) + { + _logger = logger; + _settingsService = settingsService; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Version: {version}", FileVersionInfo.GetVersionInfo(GetType().Assembly.Location).ProductVersion); + _logger.LogInformation("Preloading Configuration"); + await _settingsService.GetConfiguration(cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/ingress-nginx-validate-jwt/ISettingsService.cs b/ingress-nginx-validate-jwt/ISettingsService.cs new file mode 100644 index 0000000..0e562e0 --- /dev/null +++ b/ingress-nginx-validate-jwt/ISettingsService.cs @@ -0,0 +1,9 @@ +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace ingress_nginx_validate_jwt +{ + public interface ISettingsService + { + Task GetConfiguration(CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/ingress-nginx-validate-jwt/Program.cs b/ingress-nginx-validate-jwt/Program.cs new file mode 100644 index 0000000..eb9dca7 --- /dev/null +++ b/ingress-nginx-validate-jwt/Program.cs @@ -0,0 +1,36 @@ +using Prometheus; +using System.Diagnostics.CodeAnalysis; +using System.IdentityModel.Tokens.Jwt; + +namespace ingress_nginx_validate_jwt; + +[ExcludeFromCodeCoverage] +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + + builder.Services.AddControllers(); + + builder.Services.AddSingleton(); + + builder.Services.AddTransient(); + + builder.Services.AddHostedService(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + + app.UseForwardedHeaders(); + + app.MapControllers(); + + app.UseMetricServer(); + + app.Run(); + } +} \ No newline at end of file diff --git a/ingress-nginx-validate-jwt/Properties/launchSettings.json b/ingress-nginx-validate-jwt/Properties/launchSettings.json new file mode 100644 index 0000000..1736aad --- /dev/null +++ b/ingress-nginx-validate-jwt/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "ingress_nginx_validate_jwt": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7297;http://localhost:5049" + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/ingress-nginx-validate-jwt/SettingsService.cs b/ingress-nginx-validate-jwt/SettingsService.cs new file mode 100644 index 0000000..bee5b50 --- /dev/null +++ b/ingress-nginx-validate-jwt/SettingsService.cs @@ -0,0 +1,41 @@ +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace ingress_nginx_validate_jwt; + +public class SettingsService : ISettingsService +{ + private ILogger _logger; + + private IConfiguration _configuration; + + private OpenIdConnectConfiguration? openIdConnectConfiguration; + + public SettingsService(ILogger logger, IConfiguration configuration) + { + _logger = logger; + _configuration = configuration; + } + + public async Task GetConfiguration(CancellationToken cancellationToken = new CancellationToken()) + { + if (openIdConnectConfiguration == null) + { + string? configEndpoint = _configuration["OpenIdProviderConfigurationUrl"]; + + if (string.IsNullOrEmpty(configEndpoint)) + { + var exp = new Exception("Unable to load OpenIdConfiguration"); + + _logger.LogError(exp, "Unable to load OpenIdConfiguration"); + + throw exp; + } + + _logger.LogInformation("Loading OpenIdConfiguration from : {config}", configEndpoint); + + openIdConnectConfiguration = await OpenIdConnectConfigurationRetriever.GetAsync(configEndpoint, cancellationToken); + } + + return openIdConnectConfiguration; + } +} diff --git a/ingress-nginx-validate-jwt/appsettings.Development.json b/ingress-nginx-validate-jwt/appsettings.Development.json new file mode 100644 index 0000000..facfd1b --- /dev/null +++ b/ingress-nginx-validate-jwt/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "OpenIdProviderConfigurationUrl": "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Hosting.Lifetime": "Critical" + } + }, + "AllowedHosts": "*" +} diff --git a/ingress-nginx-validate-jwt/appsettings.json b/ingress-nginx-validate-jwt/appsettings.json new file mode 100644 index 0000000..7ed907f --- /dev/null +++ b/ingress-nginx-validate-jwt/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Hosting.Lifetime": "Critical" + } + }, + "AllowedHosts": "*" +} diff --git a/ingress-nginx-validate-jwt/ingress-nginx-validate-jwt.csproj b/ingress-nginx-validate-jwt/ingress-nginx-validate-jwt.csproj new file mode 100644 index 0000000..8777f0b --- /dev/null +++ b/ingress-nginx-validate-jwt/ingress-nginx-validate-jwt.csproj @@ -0,0 +1,28 @@ + + + + net7.0 + enable + enable + ingress_nginx_validate_jwt + 1c55899c-42df-4a18-815d-93afa873bb49 + Linux + true + + + + + + + Never + + + + + + + + + + +