├── .editorconfig
├── .github
├── dependabot.yml
├── release-drafter.yml
├── settings.xml
└── workflows
│ ├── build-master.yml
│ ├── codeql-analysis.yml
│ ├── dependabot-auto-merge.yml
│ ├── publish-release.yml
│ ├── sonarcloud.yml
│ └── test-pull-requests.yml
├── .gitignore
├── .java-version
├── .mvn
└── wrapper
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── CODEOWNERS
├── LICENSE
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── token-client-core
├── pom.xml
└── src
│ ├── main
│ └── kotlin
│ │ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── client
│ │ └── core
│ │ ├── ClientAuthenticationProperties.kt
│ │ ├── ClientProperties.kt
│ │ ├── OAuth2CacheFactory.kt
│ │ ├── OAuth2ClientException.kt
│ │ ├── OAuth2GrantType.kt
│ │ ├── OAuth2ParameterNames.kt
│ │ ├── auth
│ │ └── ClientAssertion.kt
│ │ ├── context
│ │ └── JwtBearerTokenResolver.kt
│ │ ├── http
│ │ ├── OAuth2HttpClient.kt
│ │ ├── OAuth2HttpHeaders.kt
│ │ ├── OAuth2HttpRequest.kt
│ │ └── SimpleOAuth2HttpClient.kt
│ │ ├── jwk
│ │ └── JwkFactory.kt
│ │ └── oauth2
│ │ ├── AbstractOAuth2GrantRequest.kt
│ │ ├── AbstractOAuth2TokenClient.kt
│ │ ├── ClientCredentialsGrantRequest.kt
│ │ ├── ClientCredentialsTokenClient.kt
│ │ ├── OAuth2AccessTokenResponse.kt
│ │ ├── OAuth2AccessTokenService.kt
│ │ ├── OnBehalfOfGrantRequest.kt
│ │ ├── OnBehalfOfTokenClient.kt
│ │ ├── TokenExchangeClient.kt
│ │ └── TokenExchangeGrantRequest.kt
│ └── test
│ ├── kotlin
│ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── client
│ │ └── core
│ │ ├── ClientAuthenticationPropertiesTest.kt
│ │ ├── ClientPropertiesTest.kt
│ │ ├── TestUtils.kt
│ │ ├── auth
│ │ └── ClientAssertionTest.kt
│ │ ├── http
│ │ └── OAuth2HttpHeadersTest.kt
│ │ ├── jwk
│ │ └── JwkFactoryTest.kt
│ │ └── oauth2
│ │ ├── ClientCredentialsTokenClientTest.kt
│ │ ├── OAuth2AccessTokenServiceTest.kt
│ │ ├── OnBehalfOfTokenClientTest.kt
│ │ └── TokenExchangeClientTest.kt
│ └── resources
│ ├── jwk.json
│ ├── logback-test.xml
│ └── selfsigned.jks
├── token-client-kotlin-demo
├── .gitignore
├── pom.xml
└── src
│ ├── main
│ ├── kotlin
│ │ └── no
│ │ │ └── nav
│ │ │ └── security
│ │ │ └── token
│ │ │ └── support
│ │ │ └── ktor
│ │ │ ├── Application.kt
│ │ │ └── oauth
│ │ │ ├── ClientConfig.kt
│ │ │ ├── OAuth2Cache.kt
│ │ │ └── OAuth2Client.kt
│ └── resources
│ │ ├── application.conf
│ │ ├── jwk.json
│ │ └── logback.xml
│ └── test
│ └── kotlin
│ └── no
│ └── nav
│ └── security
│ └── token
│ └── support
│ └── ktor
│ ├── ApplicationTest.kt
│ └── oauth
│ └── OAuth2ClientIntegrationTest.kt
├── token-client-spring-demo
├── .gitignore
├── .mvn
│ └── wrapper
│ │ ├── maven-wrapper.jar
│ │ └── maven-wrapper.properties
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
│ └── main
│ ├── kotlin
│ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── demo
│ │ └── spring
│ │ ├── DemoApplication.kt
│ │ ├── client
│ │ ├── DemoClient1.kt
│ │ ├── DemoClient2.kt
│ │ └── DemoClient3.kt
│ │ ├── config
│ │ └── DemoConfiguration.kt
│ │ ├── mockwebserver
│ │ └── MockWebServerConfiguration.kt
│ │ └── rest
│ │ └── DemoController.kt
│ └── resources
│ ├── application.yaml
│ └── jwk.json
├── token-client-spring
├── pom.xml
└── src
│ ├── main
│ └── kotlin
│ │ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── client
│ │ └── spring
│ │ ├── ClientConfigurationProperties.kt
│ │ └── oauth2
│ │ ├── ClientConfigurationPropertiesMatcher.kt
│ │ ├── DefaultOAuth2HttpClient.kt
│ │ ├── EnableOAuth2Client.kt
│ │ ├── OAuth2ClientConfiguration.kt
│ │ └── OAuth2ClientRequestInterceptor.kt
│ └── test
│ ├── kotlin
│ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── client
│ │ └── spring
│ │ └── oauth2
│ │ ├── ClientConfigurationPropertiesTest.kt
│ │ ├── ClientConfigurationPropertiesTestWithResourceUrl.kt
│ │ ├── ClientConfigurationPropertiesTestWithWellKnownUrl.kt
│ │ ├── DefaultOAuth2HttpClientTest.kt
│ │ ├── OAuth2AccessTokenServiceIntegrationTest.kt
│ │ ├── OAuth2ClientConfigurationWithCacheTest.kt
│ │ ├── OAuth2ClientConfigurationWithoutCacheTest.kt
│ │ └── TestUtils.kt
│ └── resources
│ ├── application-test-withresourceurl.yml
│ ├── application-test-withwellknownurl.yml
│ ├── application-test.yml
│ ├── banner.txt
│ └── jwk.json
├── token-validation-core
├── .gitignore
├── pom.xml
└── src
│ ├── main
│ └── kotlin
│ │ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── core
│ │ ├── JwtTokenConstants.kt
│ │ ├── api
│ │ ├── Protected.kt
│ │ ├── ProtectedWithClaims.kt
│ │ ├── RequiredIssuers.kt
│ │ └── Unprotected.kt
│ │ ├── configuration
│ │ ├── IssuerConfiguration.kt
│ │ ├── IssuerProperties.kt
│ │ ├── MultiIssuerConfiguration.kt
│ │ └── ProxyAwareResourceRetriever.kt
│ │ ├── context
│ │ ├── TokenValidationContext.kt
│ │ └── TokenValidationContextHolder.kt
│ │ ├── exceptions
│ │ ├── AnnotationRequiredException.kt
│ │ ├── IssuerConfigurationException.kt
│ │ ├── JwtTokenInvalidClaimException.kt
│ │ ├── JwtTokenMissingException.kt
│ │ ├── JwtTokenValidatorException.kt
│ │ └── MetaDataNotAvailableException.kt
│ │ ├── http
│ │ └── HttpRequest.kt
│ │ ├── jwt
│ │ ├── JwtToken.kt
│ │ └── JwtTokenClaims.kt
│ │ ├── utils
│ │ ├── Cluster.kt
│ │ ├── EnvUtil.kt
│ │ └── JwtTokenUtil.kt
│ │ └── validation
│ │ ├── DefaultConfigurableJwtValidator.kt
│ │ ├── DefaultJwtClaimsVerifier.kt
│ │ ├── JwtTokenAnnotationHandler.kt
│ │ ├── JwtTokenRetriever.kt
│ │ ├── JwtTokenValidationHandler.kt
│ │ ├── JwtTokenValidator.kt
│ │ └── JwtTokenValidatorFactory.kt
│ └── test
│ ├── kotlin
│ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── core
│ │ ├── IssuerMockWebServer.kt
│ │ ├── configuration
│ │ ├── IssuerConfigurationTest.kt
│ │ ├── MultiIssuerConfigurationTest.kt
│ │ └── ProxyAwareResourceRetrieverTest.kt
│ │ ├── context
│ │ └── TokenValidationContextTest.kt
│ │ ├── jwt
│ │ └── JwtTokenClaimsTest.kt
│ │ └── validation
│ │ ├── AbstractJwtValidatorTest.kt
│ │ ├── DefaultConfigurableJwtValidatorTest.kt
│ │ ├── JwtTokenAnnotationHandlerTest.kt
│ │ ├── JwtTokenRetrieverTest.kt
│ │ └── JwtTokenValidatorFactoryTest.kt
│ └── resources
│ ├── logback-test.xml
│ └── metadata.json
├── token-validation-filter
├── .gitignore
├── pom.xml
└── src
│ ├── main
│ └── kotlin
│ │ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── filter
│ │ ├── JwtTokenExpiryFilter.kt
│ │ └── JwtTokenValidationFilter.kt
│ └── test
│ ├── kotlin
│ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── filter
│ │ ├── JwtTokenExpiryFilterTest.kt
│ │ └── JwtTokenValidationFilterTest.kt
│ └── resources
│ ├── logback-test.xml
│ └── mockmetadata.json
├── token-validation-jaxrs
├── pom.xml
└── src
│ ├── main
│ └── kotlin
│ │ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── jaxrs
│ │ ├── JaxrsTokenValidationContextHolder.kt
│ │ ├── JwtTokenClientRequestFilter.kt
│ │ ├── JwtTokenContainerRequestFilter.kt
│ │ └── servlet
│ │ └── JaxrsJwtTokenValidationFilter.kt
│ └── test
│ ├── kotlin
│ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── jaxrs
│ │ ├── ClientFilterTest.kt
│ │ ├── Config.kt
│ │ ├── FileResourceRetriever.kt
│ │ ├── JwkGenerator.kt
│ │ ├── JwtTokenGenerator.kt
│ │ ├── ServerFilterProtectedClassTest.kt
│ │ ├── ServerFilterProtectedClassUnknownIssuerTest.kt
│ │ ├── ServerFilterProtectedMethodTest.kt
│ │ ├── ServerFilterProtectedMethodUnknownIssuerTest.kt
│ │ ├── TestTokenGeneratorResource.kt
│ │ └── rest
│ │ ├── ProtectedClassResource.kt
│ │ ├── ProtectedMethodResource.kt
│ │ ├── ProtectedWithClaimsClassResource.kt
│ │ ├── TokenResource.kt
│ │ ├── UnprotectedClassResource.kt
│ │ └── WithoutAnnotationsResource.kt
│ └── resources
│ ├── application-invalid.yaml
│ ├── application-protected.yaml
│ ├── jwkset.json
│ ├── jwtkeystore.jks
│ ├── logback.xml
│ └── metadata.json
├── token-validation-ktor-demo
├── .gitignore
├── pom.xml
├── resources
│ ├── application.conf
│ └── logback.xml
└── src
│ ├── main
│ └── kotlin
│ │ └── Application.kt
│ └── test
│ └── kotlin
│ └── ApplicationTokenTest.kt
├── token-validation-ktor-v2
├── .gitignore
├── pom.xml
└── src
│ ├── main
│ └── kotlin
│ │ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── v2
│ │ ├── JwtTokenExpiryThresholdHandler.kt
│ │ └── TokenSupportAuthenticationProvider.kt
│ └── test
│ ├── kotlin
│ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── v2
│ │ ├── ApplicationTest.kt
│ │ ├── InlineConfigTest.kt
│ │ ├── JwkGenerator.kt
│ │ ├── JwtTokenGenerator.kt
│ │ ├── TokenSupportAuthenticationProviderKtTest.kt
│ │ ├── inlineconfigtestapp
│ │ └── InlineConfigApplication.kt
│ │ └── testapp
│ │ └── TestApplication.kt
│ └── resources
│ └── jwkset.json
├── token-validation-ktor-v3
├── .gitignore
├── pom.xml
└── src
│ ├── main
│ └── kotlin
│ │ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── v3
│ │ ├── JwtTokenExpiryThresholdHandler.kt
│ │ └── TokenSupportAuthenticationProvider.kt
│ └── test
│ ├── kotlin
│ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── v3
│ │ ├── ApplicationTest.kt
│ │ ├── InlineConfigTest.kt
│ │ ├── JwkGenerator.kt
│ │ ├── JwtTokenGenerator.kt
│ │ ├── TokenSupportAuthenticationProviderKtTest.kt
│ │ ├── inlineconfigtestapp
│ │ └── InlineConfigApplication.kt
│ │ └── testapp
│ │ └── TestApplication.kt
│ └── resources
│ └── jwkset.json
├── token-validation-spring-demo
├── .gitignore
├── .mvn
│ └── wrapper
│ │ ├── maven-wrapper.jar
│ │ └── maven-wrapper.properties
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
│ ├── main
│ ├── kotlin
│ │ └── no
│ │ │ └── nav
│ │ │ └── security
│ │ │ └── token
│ │ │ └── support
│ │ │ └── demo
│ │ │ └── spring
│ │ │ ├── DemoApplication.kt
│ │ │ ├── config
│ │ │ └── SecurityConfiguration.kt
│ │ │ └── rest
│ │ │ └── DemoController.kt
│ └── resources
│ │ ├── META-INF
│ │ └── additional-spring-configuration-metadata.json
│ │ └── application.yaml
│ └── test
│ ├── kotlin
│ └── no
│ │ └── nav
│ │ └── security
│ │ └── token
│ │ └── support
│ │ └── demo
│ │ └── spring
│ │ ├── LocalDemoApplication.kt
│ │ └── LocalSecurityConfiguration.kt
│ └── resources
│ ├── application-local.yaml
│ └── application-test.yaml
├── token-validation-spring-test
├── README.md
├── pom.xml
└── src
│ ├── main
│ ├── kotlin
│ │ └── no
│ │ │ └── nav
│ │ │ └── security
│ │ │ └── token
│ │ │ └── support
│ │ │ └── spring
│ │ │ └── test
│ │ │ ├── EnableMockOAuth2Server.kt
│ │ │ ├── MockLoginController.kt
│ │ │ ├── MockOAuth2ServerApplicationListener.kt
│ │ │ └── MockOAuth2ServerAutoConfiguration.kt
│ └── resources
│ │ └── META-INF
│ │ └── spring.factories
│ └── test
│ └── kotlin
│ └── no
│ └── nav
│ └── security
│ └── token
│ └── support
│ └── spring
│ └── test
│ ├── EnableMockOAuth2ServerRandomPortTest.kt
│ ├── EnableMockOAuth2ServerRandomStaticPortTest.kt
│ └── TestApplication.kt
└── token-validation-spring
├── .gitignore
├── pom.xml
└── src
├── main
└── kotlin
│ └── no
│ └── nav
│ └── security
│ └── token
│ └── support
│ └── spring
│ ├── EnableJwtTokenValidationConfiguration.kt
│ ├── MultiIssuerProperties.kt
│ ├── ProtectedRestController.kt
│ ├── SpringTokenValidationContextHolder.kt
│ ├── api
│ └── EnableJwtTokenValidation.kt
│ └── validation
│ └── interceptor
│ ├── BearerTokenClientHttpRequestInterceptor.kt
│ ├── JwtTokenHandlerInterceptor.kt
│ ├── JwtTokenUnauthorizedException.kt
│ └── SpringJwtTokenAnnotationHandler.kt
└── test
├── kotlin
└── no
│ └── nav
│ └── security
│ └── token
│ └── support
│ └── spring
│ ├── MultiIssuerConfigurationPropertiesTest.kt
│ ├── integrationtest
│ ├── AProtectedRestController.kt
│ ├── JWKGenerator.kt
│ ├── JWTTokenGenerator.kt
│ ├── ProtectedApplication.kt
│ ├── ProtectedApplicationConfig.kt
│ └── ProtectedRestControllerIntegrationTest.kt
│ └── validation
│ └── interceptor
│ ├── JwtTokenHandlerInterceptorTest.kt
│ └── MetaAnnotations.kt
└── resources
├── application.yaml
├── issuers.properties
└── jwtkeystore.jks
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.java]
4 | indent_style = space
5 | indent_size = 4
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: github-actions
9 | directory: "/"
10 | schedule:
11 | interval: daily
12 | open-pull-requests-limit: 10
13 | - package-ecosystem: "maven" # See documentation for possible values
14 | directory: "/" # Location of package manifests
15 | schedule:
16 | interval: daily
17 | open-pull-requests-limit: 10
18 |
19 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: $NEXT_PATCH_VERSION
2 | tag-template: $NEXT_PATCH_VERSION
3 | change-template: '- $TITLE (#$NUMBER) @$AUTHOR'
4 | categories:
5 | - title: '🚀 Features'
6 | labels:
7 | - 'feature'
8 | - 'enhancement'
9 | - title: '⚠️ Breaking Changes'
10 | labels:
11 | - 'breaking'
12 | - title: '🐛 Bug Fixes'
13 | labels:
14 | - 'fix'
15 | - 'bugfix'
16 | - 'bug'
17 | - title: '🧰 Maintenance'
18 | labels:
19 | - 'chore'
20 | - title: '⬆️ Dependency upgrades'
21 | labels:
22 | - 'bump'
23 | - 'dependencies'
24 | exclude-labels:
25 | - 'skip-changelog'
26 | template: |
27 | ## What's Changed
28 | $CHANGES
29 |
--------------------------------------------------------------------------------
/.github/settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | org.jacoco
4 |
5 |
6 |
7 | ossrh
8 | ${env.SONATYPE_USER}
9 | ${env.SONATYPE_PASSWORD}
10 |
11 |
12 | github
13 | ${env.GITHUB_USERNAME}
14 | ${env.GITHUB_PASSWORD}
15 |
16 |
17 |
18 |
19 | ossrh
20 |
21 | true
22 |
23 |
24 | gpg
25 | ${env.GPG_KEYNAME}
26 | ${env.GPG_PASSPHRASE}
27 |
28 |
29 |
30 | github
31 |
32 |
33 | central
34 | https://repo1.maven.org/maven2
35 |
36 | true
37 |
38 |
39 | true
40 |
41 |
42 |
43 | github
44 |
45 | true
46 |
47 |
48 | true
49 |
50 | gitHub
51 | https://maven.pkg.github.com/navikt/token-support/
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/.github/workflows/build-master.yml:
--------------------------------------------------------------------------------
1 | name: Build master
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | permissions:
8 | checks: write
9 | actions: read
10 | contents: write
11 | security-events: write
12 | packages: write
13 | id-token: write
14 |
15 | jobs:
16 | build:
17 | runs-on: ubuntu-latest-4-cores
18 | steps:
19 | - name: Checkout latest code
20 | uses: actions/checkout@v4
21 |
22 | - name: Set up JDK 21
23 | uses: actions/setup-java@v4
24 | with:
25 | java-version: 21
26 | distribution: temurin
27 | - name: Setup build cache
28 | uses: actions/cache@v4
29 | with:
30 | path: ~/.m2/repository
31 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
32 | restore-keys: |
33 | ${{ runner.os }}-maven-
34 |
35 | - name: Submit Dependency Snapshot
36 | uses: advanced-security/maven-dependency-submission-action@v5
37 | with:
38 | settings-file: .github/settings.xml
39 |
40 | - name: Build with Maven
41 | env:
42 | GITHUB_USERNAME: x-access-token
43 | GITHUB_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
46 | run: ./mvnw package --settings .github/settings.xml -Pgithub
47 |
48 | - name: Publish Test Report
49 | uses: mikepenz/action-junit-report@v5
50 | if: always() # always run even if the previous step fails
51 | with:
52 | include_time_in_summary: 'true'
53 | include_passed: 'true'
54 | detailed_summary: 'true'
55 | report_paths: '**/build/test-results/test/TEST-*.xml'
56 |
57 |
58 | release-notes:
59 | runs-on: ubuntu-latest
60 | steps:
61 | - name: Release Drafter
62 | uses: release-drafter/release-drafter@v6
63 | env:
64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [ "main" ]
9 | schedule:
10 | - cron: '17 3 * * 0'
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 | permissions:
17 | actions: read
18 | contents: read
19 | security-events: write
20 |
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | language: [ 'java' ]
25 |
26 | steps:
27 | - name: Checkout repository
28 | uses: actions/checkout@v4
29 |
30 | - name: Setter opp Java 21
31 | uses: actions/setup-java@v4
32 | with:
33 | java-version: 21
34 | distribution: temurin
35 | cache: maven
36 |
37 | # Initializes the CodeQL tools for scanning.
38 | - name: Initialize CodeQL
39 | uses: github/codeql-action/init@v3
40 | with:
41 | languages: ${{ matrix.language }}
42 | queries: security-extended,security-and-quality
43 |
44 | - name: Kompilerer
45 | id: kompiler
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 | run: ./mvnw --settings .github/settings.xml -Drevision=${TAG} package
49 |
50 | - name: Perform CodeQL Analysis
51 | uses: github/codeql-action/analyze@v3
52 | with:
53 | category: "/language:${{matrix.language}}"
--------------------------------------------------------------------------------
/.github/workflows/dependabot-auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot auto-merge
2 | on: pull_request
3 |
4 | permissions:
5 | contents: write
6 | pull-requests: write
7 |
8 | jobs:
9 | dependabot:
10 | runs-on: ubuntu-latest
11 | if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}
12 | steps:
13 | - name: Dependabot metadata
14 | id: metadata
15 | uses: dependabot/fetch-metadata@v2
16 | with:
17 | github-token: "${{ secrets.GITHUB_TOKEN }}"
18 | - name: Enable auto-merge for Dependabot PRs
19 | if: steps.metadata.outputs.update-type != 'version-update:semver-major'
20 | run: gh pr merge --auto --squash "$PR_URL"
21 | env:
22 | PR_URL: ${{github.event.pull_request.html_url}}
23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24 |
--------------------------------------------------------------------------------
/.github/workflows/publish-release.yml:
--------------------------------------------------------------------------------
1 | name: Publish release
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | publish-release:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: read
12 | packages: write
13 | steps:
14 | - name: Checkout latest code
15 | uses: actions/checkout@v4
16 |
17 | - name: Set up JDK 21
18 | uses: actions/setup-java@v4
19 | with:
20 | java-version: 21
21 | distribution: temurin
22 | cache: maven
23 |
24 | - name: Setup build cache
25 | uses: actions/cache@v4
26 | with:
27 | path: ~/.m2/repository
28 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
29 | restore-keys: |
30 | ${{ runner.os }}-maven-
31 |
32 | - name: Set new version and publish artifact to central
33 | env:
34 | NEW_VERSION: ${{ github.event.release.tag_name }}
35 | SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
36 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
37 | GPG_KEYNAME: ${{ secrets.GPG_KEYNAME }}
38 | GPG_KEYS: ${{ secrets.GPG_KEYS }}
39 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
40 | run: |
41 | export GPG_TTY=$(tty) && echo "$GPG_KEYS" | gpg --fast-import --batch
42 | echo "-- Setting new release version ${NEW_VERSION} --"
43 | ./mvnw -B versions:set -DnewVersion="${NEW_VERSION}" -DgenerateBackupPoms=false
44 | echo "-- Build, test and deploy release to Sonatype --"
45 | ./mvnw -B --settings .github/settings.xml clean deploy -Prelease,deploy-to-sonatype -Dmaven.wagon.http.pool=false
46 |
47 | - name: Publish artifact to GPR
48 | env:
49 | GITHUB_USERNAME: ${{ github.actor }}
50 | GITHUB_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52 | run: |
53 | ./mvnw -Pgithub --settings .github/settings.xml --batch-mode -DskipTests -Dmaven.main.skip=true -Dmaven.test.skip=true deploy
--------------------------------------------------------------------------------
/.github/workflows/sonarcloud.yml:
--------------------------------------------------------------------------------
1 | name: Sonar
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | env:
8 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
9 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
10 |
11 | jobs:
12 | bygg:
13 | runs-on: ubuntu-latest
14 | steps:
15 |
16 | - name: Sjekk ut kode
17 | uses: actions/checkout@v4
18 |
19 | - name: Sett opp Java 21
20 | uses: actions/setup-java@v4
21 | with:
22 | java-version: 21
23 | distribution: temurin
24 | cache: maven
25 | - name: Analyser
26 | run: |
27 | ./mvnw versions:set -DnewVersion=${TAG}
28 | ./mvnw --settings .github/settings.xml verify jacoco:prepare-agent jacoco:report org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.login=${SONAR_TOKEN} -Dsonar.projectKey=navikt_token-support
--------------------------------------------------------------------------------
/.github/workflows/test-pull-requests.yml:
--------------------------------------------------------------------------------
1 | name: Test pull requests
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout latest code
10 | uses: actions/checkout@v4
11 | with:
12 | fetch-depth: 0
13 |
14 | - name: Set up JDK 21
15 | uses: actions/setup-java@v4
16 | with:
17 | java-version: 21
18 | distribution: temurin
19 |
20 | - name: Setup build cache
21 | uses: actions/cache@v4
22 | with:
23 | path: ~/.m2/repository
24 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
25 | restore-keys: |
26 | ${{ runner.os }}-maven-
27 | - name: Build with Maven
28 | env:
29 | GITHUB_USERNAME: x-access-token
30 | GITHUB_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
31 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33 | run: mvn -B test
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | .DS_Store
5 |
6 | ### STS ###
7 | .apt_generated
8 | .classpath
9 | .factorypath
10 | .project
11 | .settings
12 | .springBeans
13 |
14 | ### IntelliJ IDEA ###
15 | .idea
16 | *.iws
17 | *.iml
18 | *.ipr
19 |
20 | ### NetBeans ###
21 | nbproject/private/
22 | build/
23 | nbbuild/
24 | dist/
25 | nbdist/
26 | .nb-gradle/
27 |
28 | **/.project
29 | **/.settings
30 | **/.classpath
31 | **/target
--------------------------------------------------------------------------------
/.java-version:
--------------------------------------------------------------------------------
1 | 21.0.1
2 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/navikt/token-support/731015c17f10aa7d9286dea5b0480c1837d7944e/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
19 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @navikt/pig-sikkerhet
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 NAV IKT
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/ClientAuthenticationProperties.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core
2 |
3 | import com.nimbusds.jose.jwk.RSAKey
4 | import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod
5 | import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC
6 | import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_POST
7 | import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.PRIVATE_KEY_JWT
8 | import no.nav.security.token.support.client.core.jwk.JwkFactory.fromJson
9 | import no.nav.security.token.support.client.core.jwk.JwkFactory.fromJsonFile
10 |
11 | data class ClientAuthenticationProperties @JvmOverloads constructor(val clientId: String, val clientAuthMethod: ClientAuthenticationMethod,val clientSecret: String?,val clientJwk: String? = null, val clientRsaKey: RSAKey? = loadKey(clientJwk)) {
12 |
13 | init {
14 | require(clientAuthMethod in CLIENT_AUTH_METHODS) {
15 | "Unsupported authentication method $clientAuthMethod, must be one of $CLIENT_AUTH_METHODS"
16 | }
17 | if (clientAuthMethod in listOf(CLIENT_SECRET_BASIC, CLIENT_SECRET_POST)) {
18 | requireNotNull(clientSecret) { "Client secret must be set for authentication method $clientAuthMethod" }
19 | }
20 | if (PRIVATE_KEY_JWT.equals(clientAuthMethod)) {
21 | requireNotNull(clientJwk) { "Client private key must be set for authentication method $clientAuthMethod" }
22 | }
23 | }
24 |
25 | companion object {
26 | private val CLIENT_AUTH_METHODS = listOf(CLIENT_SECRET_BASIC, CLIENT_SECRET_POST, PRIVATE_KEY_JWT)
27 |
28 | @JvmStatic
29 | fun builder(clientId: String, clientAuthMethod: ClientAuthenticationMethod) = ClientAuthenticationPropertiesBuilder(clientId, clientAuthMethod)
30 | private fun loadKey(clientJwk: String?) =
31 | clientJwk?.let {
32 | if (it.startsWith("{")) {
33 | fromJson(it)
34 | } else {
35 | fromJsonFile(it)
36 | }
37 | }
38 | }
39 |
40 | }
41 |
42 | class ClientAuthenticationPropertiesBuilder @JvmOverloads constructor(private val clientId: String, private val clientAuthMethod: ClientAuthenticationMethod, private var clientSecret: String? = null, private var clientJwk: String? = null) {
43 | fun clientSecret(clientSecret: String)= this.also { it.clientSecret = clientSecret }
44 | fun clientJwk(clientJwk: String)= this.also { it.clientJwk = clientJwk }
45 | fun build() = ClientAuthenticationProperties(clientId, clientAuthMethod, clientSecret, clientJwk)
46 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2CacheFactory.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core
2 |
3 | import com.github.benmanes.caffeine.cache.Caffeine
4 | import com.github.benmanes.caffeine.cache.Expiry
5 | import java.util.concurrent.TimeUnit.SECONDS
6 | import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse
7 |
8 | object OAuth2CacheFactory {
9 |
10 | @JvmStatic
11 | fun accessTokenResponseCache(maximumSize : Long, skewInSeconds : Long) =
12 | // Evict based on a varying expiration policy
13 | Caffeine.newBuilder()
14 | .maximumSize(maximumSize)
15 | .expireAfter(evictOnResponseExpiresIn(skewInSeconds))
16 | .build()
17 |
18 | private fun evictOnResponseExpiresIn(skewInSeconds : Long) : Expiry {
19 | return object : Expiry {
20 | override fun expireAfterCreate(key : T, response : OAuth2AccessTokenResponse, currentTime : Long) =
21 | SECONDS.toNanos(if (response.expires_in!! > skewInSeconds) response.expires_in!! - skewInSeconds else response.expires_in!!.toLong())
22 | override fun expireAfterUpdate(key : T, response : OAuth2AccessTokenResponse, currentTime : Long, currentDuration : Long) = currentDuration
23 | override fun expireAfterRead(key : T, response : OAuth2AccessTokenResponse, currentTime : Long, currentDuration : Long) = currentDuration
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2ClientException.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core
2 |
3 | class OAuth2ClientException @JvmOverloads constructor (message : String?, cause : Throwable? = null) : RuntimeException(message, cause)
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2GrantType.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core
2 |
3 | import com.nimbusds.oauth2.sdk.GrantType
4 | import kotlin.DeprecationLevel.WARNING
5 |
6 | @Deprecated("Use GrantType from nimbus instead", ReplaceWith("GrantType"), WARNING)
7 | data class OAuth2GrantType(val value : String) {
8 | companion object {
9 | @JvmField
10 | @Deprecated("Use com.nimbusds.oauth2.sdk.GrantType instead", ReplaceWith("GrantType.JWT_BEARER"), WARNING)
11 | val JWT_BEARER = GrantType(GrantType.JWT_BEARER.value)
12 | @JvmField
13 | @Deprecated("Use com.nimbusds.oauth2.sdk.GrantType instead", ReplaceWith("GrantType.CLIENT_CREDENTIALS"), WARNING)
14 | val CLIENT_CREDENTIALS = GrantType(GrantType.CLIENT_CREDENTIALS.value)
15 | @JvmField
16 | @Deprecated("Use com.nimbusds.oauth2.sdk.GrantType instead", ReplaceWith("GrantType.TOKEN_EXCHANGE"), WARNING)
17 | val TOKEN_EXCHANGE = GrantType(GrantType.TOKEN_EXCHANGE.value)
18 | }
19 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2ParameterNames.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core
2 |
3 | object OAuth2ParameterNames {
4 |
5 | const val GRANT_TYPE = "grant_type"
6 | const val CLIENT_ID = "client_id"
7 | const val CLIENT_SECRET = "client_secret"
8 | const val ASSERTION = "assertion"
9 | const val REQUESTED_TOKEN_USE = "requested_token_use"
10 | const val SCOPE = "scope"
11 | const val CLIENT_ASSERTION_TYPE = "client_assertion_type"
12 | const val CLIENT_ASSERTION = "client_assertion"
13 | const val SUBJECT_TOKEN_TYPE = "subject_token_type"
14 | const val SUBJECT_TOKEN = "subject_token"
15 | const val AUDIENCE = "audience"
16 | const val RESOURCE = "resource"
17 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/auth/ClientAssertion.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.auth
2 |
3 | import com.nimbusds.jose.JOSEObjectType.JWT
4 | import com.nimbusds.jose.JWSAlgorithm.RS256
5 | import com.nimbusds.jose.JWSHeader
6 | import com.nimbusds.jose.crypto.RSASSASigner
7 | import com.nimbusds.jose.jwk.RSAKey
8 | import com.nimbusds.jwt.JWTClaimNames.JWT_ID
9 | import com.nimbusds.jwt.JWTClaimsSet
10 | import com.nimbusds.jwt.JWTClaimsSet.Builder
11 | import com.nimbusds.jwt.SignedJWT
12 | import com.nimbusds.oauth2.sdk.auth.JWTAuthentication.CLIENT_ASSERTION_TYPE
13 | import java.net.URI
14 | import java.time.Instant.now
15 | import java.util.*
16 | import no.nav.security.token.support.client.core.ClientAuthenticationProperties
17 | import kotlin.DeprecationLevel.ERROR
18 |
19 | class ClientAssertion(private val tokenEndpointUrl : URI, private val clientId : String, private val rsaKey : RSAKey, private val expiryInSeconds : Int) {
20 | constructor(tokenEndpointUrl: URI, auth : ClientAuthenticationProperties) : this(tokenEndpointUrl, auth.clientId, auth.clientRsaKey!!, EXPIRY_IN_SECONDS)
21 |
22 | fun assertion() =
23 | now().run {
24 | createSignedJWT(rsaKey, Builder()
25 | .audience("$tokenEndpointUrl")
26 | .expirationTime(Date.from(plusSeconds(expiryInSeconds.toLong())))
27 | .issuer(clientId)
28 | .subject(clientId)
29 | .claim(JWT_ID, "${UUID.randomUUID()}")
30 | .notBeforeTime(Date.from(this))
31 | .issueTime(Date.from(this))
32 | .build()).serialize()
33 | }
34 |
35 | @Deprecated("Use com.nimbusds.oauth2.sdk.auth.JWTAuthentication instead", ReplaceWith("JWTAuthentication.CLIENT_ASSERTION_TYPE"),ERROR)
36 | fun assertionType() = CLIENT_ASSERTION_TYPE
37 |
38 | private fun createSignedJWT(rsaJwk : RSAKey, claimsSet : JWTClaimsSet) =
39 | runCatching {
40 | SignedJWT(JWSHeader.Builder(RS256)
41 | .keyID(rsaJwk.keyID)
42 | .type(JWT).build(), claimsSet).apply {
43 | sign(RSASSASigner(rsaJwk.toPrivateKey()))
44 | }
45 | }.getOrElse {
46 | throw RuntimeException(it)
47 | }
48 |
49 | companion object {
50 | private const val EXPIRY_IN_SECONDS = 60
51 | }
52 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/context/JwtBearerTokenResolver.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.context
2 |
3 | fun interface JwtBearerTokenResolver {
4 |
5 | fun token() : String?
6 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpClient.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.http
2 |
3 | import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse
4 |
5 | interface OAuth2HttpClient {
6 | fun post(req : OAuth2HttpRequest) : OAuth2AccessTokenResponse
7 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeaders.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.http
2 |
3 | import java.lang.String.CASE_INSENSITIVE_ORDER
4 | import java.util.*
5 |
6 | class OAuth2HttpHeaders (val headers : Map>) {
7 |
8 | override fun equals(other : Any?) : Boolean {
9 | if (this === other) return true
10 | if (other == null || javaClass != other.javaClass) return false
11 | val that = other as OAuth2HttpHeaders
12 | return headers == that.headers
13 | }
14 |
15 | override fun hashCode() = Objects.hash(headers)
16 |
17 | override fun toString() = "${javaClass.getSimpleName()} [headers=$headers]"
18 |
19 | class Builder(private val headers : TreeMap> = TreeMap(CASE_INSENSITIVE_ORDER)) {
20 |
21 | fun header(name : String, value : String) = this.also { headers.computeIfAbsent(name) { ArrayList(1) }.add(value) }
22 |
23 | fun build() = of(headers)
24 | }
25 |
26 | companion object {
27 |
28 | @JvmField
29 | val NONE = OAuth2HttpHeaders(emptyMap())
30 | @JvmStatic
31 | fun of(headers : Map>) = OAuth2HttpHeaders(headers)
32 |
33 | @JvmStatic
34 | fun builder() = Builder()
35 | }
36 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpRequest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.http
2 |
3 | import java.net.URI
4 | import java.util.Collections.unmodifiableMap
5 | import no.nav.security.token.support.client.core.http.OAuth2HttpHeaders.Companion.NONE
6 |
7 | class OAuth2HttpRequest(val tokenEndpointUrl : URI, val oAuth2HttpHeaders : OAuth2HttpHeaders = NONE, val formParameters : Map) {
8 |
9 |
10 | class OAuth2HttpRequestBuilder @JvmOverloads constructor(private var tokenEndpointUrl: URI,
11 | private var oAuth2HttpHeaders : OAuth2HttpHeaders = NONE,
12 | private var formParameters: MutableMap = mutableMapOf()) {
13 | fun tokenEndpointUrl(tokenEndpointUrl : URI) = this.also { it.tokenEndpointUrl = tokenEndpointUrl }
14 |
15 | fun oAuth2HttpHeaders(oAuth2HttpHeaders : OAuth2HttpHeaders) = this.also { it.oAuth2HttpHeaders = oAuth2HttpHeaders }
16 |
17 | fun formParameter(key : String, value : String) = this.also { formParameters[key] = value }
18 |
19 | fun formParameters(entries: Map) = this.also { formParameters.putAll(entries) }
20 |
21 | fun build(): OAuth2HttpRequest = OAuth2HttpRequest(tokenEndpointUrl, oAuth2HttpHeaders, unmodifiableMap(formParameters))
22 |
23 | @Override
24 | override fun toString() = "OAuth2HttpRequest.OAuth2HttpRequestBuilder(tokenEndpointUrl=$tokenEndpointUrl, oAuth2HttpHeaders=$oAuth2HttpHeaders, entries=$formParameters"
25 |
26 |
27 | }
28 | companion object {
29 | fun builder( tokenEndpointUrl: URI) = OAuth2HttpRequestBuilder(tokenEndpointUrl)
30 |
31 | }
32 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/SimpleOAuth2HttpClient.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.http
2 |
3 | import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
4 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
5 | import com.fasterxml.jackson.module.kotlin.readValue
6 | import java.net.URLEncoder
7 | import java.net.http.HttpClient.newHttpClient
8 | import java.net.http.HttpRequest
9 | import java.net.http.HttpRequest.BodyPublishers
10 | import java.net.http.HttpResponse
11 | import java.net.http.HttpResponse.BodyHandlers
12 | import java.nio.charset.StandardCharsets.UTF_8
13 | import no.nav.security.token.support.client.core.OAuth2ClientException
14 | import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse
15 |
16 | class SimpleOAuth2HttpClient : OAuth2HttpClient {
17 |
18 | override fun post(req: OAuth2HttpRequest) =
19 | HttpRequest.newBuilder().configureRequest(req)
20 | .build()
21 | .sendRequest()
22 | .processResponse()
23 |
24 | private fun HttpRequest.Builder.configureRequest(req: OAuth2HttpRequest): HttpRequest.Builder {
25 | req.oAuth2HttpHeaders.headers.forEach { (key, values) -> values.forEach { header(key, it) } }
26 | uri(req.tokenEndpointUrl)
27 | POST(BodyPublishers.ofString(req.formParameters.toUrlEncodedString()))
28 | return this
29 | }
30 |
31 | private fun HttpRequest.sendRequest() = newHttpClient().send(this, BodyHandlers.ofString())
32 | private fun HttpResponse.processResponse() =
33 | if (statusCode() in 200..299) {
34 | MAPPER.readValue(body())
35 | } else {
36 | throw OAuth2ClientException("Error response from token endpoint: ${statusCode()} ${body()}")
37 | }
38 | private fun Map.toUrlEncodedString() = entries.joinToString("&") { (key, value) -> "$key=${URLEncoder.encode(value, UTF_8)}" }
39 | companion object {
40 | private val MAPPER = jacksonObjectMapper().configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
41 | }
42 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/jwk/JwkFactory.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.jwk
2 |
3 | import com.nimbusds.jose.jwk.JWKSet.load
4 | import com.nimbusds.jose.jwk.RSAKey
5 | import com.nimbusds.jose.jwk.RSAKey.Builder
6 | import com.nimbusds.jose.jwk.RSAKey.parse
7 | import com.nimbusds.jose.util.Base64URL.encode
8 | import java.io.InputStream
9 | import java.nio.charset.StandardCharsets.UTF_8
10 | import java.nio.file.Files.readString
11 | import java.nio.file.Path.of
12 | import java.security.KeyStore
13 | import java.security.MessageDigest.getInstance
14 |
15 | object JwkFactory {
16 |
17 | @JvmStatic
18 | fun fromJsonFile(filePath : String) =
19 | runCatching {
20 | fromJson(readString(of(filePath).toAbsolutePath(), UTF_8))
21 | }.getOrElse {
22 | throw JwkInvalidException(it)
23 | }
24 |
25 |
26 | @JvmStatic
27 | fun fromJson(jwk : String) =
28 | runCatching {
29 | parse(jwk)
30 | }.getOrElse {
31 | throw JwkInvalidException(it)
32 | }
33 |
34 |
35 | @JvmStatic
36 | fun fromKeyStore(alias : String, keyStoreFile : InputStream, password : String) =
37 | with(fromKeyStore(keyStoreFile, password).getKeyByKeyId(alias) as RSAKey) {
38 | Builder(this)
39 | .keyID(getX509CertSHA1Thumbprint(this))
40 | .build()
41 | }
42 |
43 | private fun fromKeyStore(keyStoreFile : InputStream, password : String) =
44 | runCatching {
45 | KeyStore.getInstance("JKS").run {
46 | with(password.toCharArray()) {
47 | load(keyStoreFile, this)
48 | load(this@run) { this }
49 | }
50 | }
51 | }.getOrElse {
52 | throw RuntimeException(it)
53 | }
54 |
55 |
56 | private fun getX509CertSHA1Thumbprint(rsaKey: RSAKey) =
57 | runCatching {
58 | rsaKey.parsedX509CertChain.firstOrNull()?.let { cert ->
59 | createSHA1DigestBase64Url(cert.encoded)
60 | }
61 | }.getOrElse { throw RuntimeException(it) }
62 |
63 | private fun createSHA1DigestBase64Url(bytes : ByteArray) =
64 | runCatching {
65 | "${encode(getInstance("SHA-1").digest(bytes))}"
66 | }.getOrElse {
67 | throw RuntimeException(it)
68 | }
69 |
70 | class JwkInvalidException(cause : Throwable) : RuntimeException(cause)
71 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2GrantRequest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.oauth2
2 |
3 | import com.nimbusds.oauth2.sdk.GrantType
4 | import java.util.*
5 | import no.nav.security.token.support.client.core.ClientProperties
6 |
7 | abstract class AbstractOAuth2GrantRequest(val grantType : GrantType, val clientProperties : ClientProperties) {
8 |
9 | override fun equals(other : Any?) : Boolean {
10 | if (this === other) return true
11 | if (other == null || javaClass != other.javaClass) return false
12 | val that = other as AbstractOAuth2GrantRequest
13 | return grantType == that.grantType && clientProperties == that.clientProperties
14 | }
15 |
16 | fun scopes() = clientProperties.scope.joinToString(" ")
17 |
18 | override fun hashCode() = Objects.hash(grantType, clientProperties)
19 | override fun toString() = "${javaClass.getSimpleName()} [oAuth2GrantType=$grantType, clientProperties=$clientProperties]"
20 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsGrantRequest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.oauth2
2 |
3 | import com.nimbusds.oauth2.sdk.GrantType.CLIENT_CREDENTIALS
4 | import no.nav.security.token.support.client.core.ClientProperties
5 |
6 | class ClientCredentialsGrantRequest(clientProperties : ClientProperties) : AbstractOAuth2GrantRequest(CLIENT_CREDENTIALS, clientProperties)
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClient.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.oauth2
2 |
3 | import no.nav.security.token.support.client.core.OAuth2ParameterNames.SCOPE
4 | import no.nav.security.token.support.client.core.http.OAuth2HttpClient
5 |
6 | class ClientCredentialsTokenClient(oAuth2HttpClient : OAuth2HttpClient) : AbstractOAuth2TokenClient(oAuth2HttpClient) {
7 |
8 | override fun formParameters(grantRequest : ClientCredentialsGrantRequest) = LinkedHashMap().apply {
9 | put(SCOPE, grantRequest.scopes())
10 | }
11 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenResponse.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.oauth2
2 |
3 | data class OAuth2AccessTokenResponse (@get:JvmName("getAccessToken") var access_token : String? = null,
4 | @get:JvmName("getExpiresAt") var expires_at : Int? = null,
5 | @get:JvmName("getExpiresIn") var expires_in : Int? = 60,
6 | private val additionalParameters : Map = emptyMap()) {
7 |
8 |
9 | @Deprecated(message = "Ikke bruk denne", replaceWith = ReplaceWith("getAccessToken()"))
10 | fun getAccess_token() = access_token
11 | @Deprecated(message = "Ikke bruk denne", replaceWith = ReplaceWith("getExpiresAt()"))
12 | fun getExpires_at() = expires_at
13 | @Deprecated(message = "Ikke bruk denne", replaceWith = ReplaceWith("getExpiresIn()"))
14 | fun getExpires_in() = expires_in
15 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfGrantRequest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.oauth2
2 |
3 | import com.nimbusds.oauth2.sdk.GrantType.JWT_BEARER
4 | import java.util.*
5 | import no.nav.security.token.support.client.core.ClientProperties
6 |
7 | class OnBehalfOfGrantRequest(clientProperties : ClientProperties, val assertion : String) : AbstractOAuth2GrantRequest(JWT_BEARER, clientProperties) {
8 |
9 | override fun equals(other : Any?) : Boolean {
10 | if (this === other) return true
11 | if (other == null || javaClass != other.javaClass) return false
12 | if (!super.equals(other)) return false
13 | val that = other as OnBehalfOfGrantRequest
14 | return assertion == that.assertion
15 | }
16 |
17 | override fun hashCode() = Objects.hash(super.hashCode(), assertion)
18 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClient.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.oauth2
2 |
3 | import no.nav.security.token.support.client.core.OAuth2ParameterNames.ASSERTION
4 | import no.nav.security.token.support.client.core.OAuth2ParameterNames.REQUESTED_TOKEN_USE
5 | import no.nav.security.token.support.client.core.OAuth2ParameterNames.SCOPE
6 | import no.nav.security.token.support.client.core.http.OAuth2HttpClient
7 |
8 | class OnBehalfOfTokenClient(oAuth2HttpClient : OAuth2HttpClient) : AbstractOAuth2TokenClient(oAuth2HttpClient) {
9 |
10 | override fun formParameters(grantRequest : OnBehalfOfGrantRequest) =
11 | LinkedHashMap().apply {
12 | put(ASSERTION, grantRequest.assertion)
13 | put(REQUESTED_TOKEN_USE,REQUESTED_TOKEN_USE_VALUE)
14 | put(SCOPE, grantRequest.scopes())
15 |
16 | }
17 |
18 | companion object {
19 | private const val REQUESTED_TOKEN_USE_VALUE = "on_behalf_of"
20 | }
21 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeClient.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.oauth2
2 |
3 | import no.nav.security.token.support.client.core.ClientProperties.TokenExchangeProperties.Companion.SUBJECT_TOKEN_TYPE_VALUE
4 | import no.nav.security.token.support.client.core.OAuth2ParameterNames.AUDIENCE
5 | import no.nav.security.token.support.client.core.OAuth2ParameterNames.RESOURCE
6 | import no.nav.security.token.support.client.core.OAuth2ParameterNames.SUBJECT_TOKEN
7 | import no.nav.security.token.support.client.core.OAuth2ParameterNames.SUBJECT_TOKEN_TYPE
8 | import no.nav.security.token.support.client.core.http.OAuth2HttpClient
9 |
10 | class TokenExchangeClient(oAuth2HttpClient : OAuth2HttpClient) : AbstractOAuth2TokenClient(oAuth2HttpClient) {
11 |
12 | override fun formParameters(grantRequest : TokenExchangeGrantRequest) =
13 | with(grantRequest) {
14 | HashMap().apply {
15 | clientProperties.tokenExchange?.run {
16 | put(SUBJECT_TOKEN_TYPE, SUBJECT_TOKEN_TYPE_VALUE)
17 | put(SUBJECT_TOKEN,subjectToken)
18 | put(AUDIENCE, audience)
19 | resource?.takeIf { it.isNotEmpty() }?.let { put(RESOURCE, it) }
20 | }
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.oauth2
2 |
3 | import com.nimbusds.oauth2.sdk.GrantType.TOKEN_EXCHANGE
4 | import java.util.*
5 | import no.nav.security.token.support.client.core.ClientProperties
6 |
7 | class TokenExchangeGrantRequest(clientProperties : ClientProperties, val subjectToken : String) : AbstractOAuth2GrantRequest(TOKEN_EXCHANGE, clientProperties) {
8 |
9 | override fun equals(other : Any?) : Boolean {
10 | if (this === other) return true
11 | if (other == null || javaClass != other.javaClass) return false
12 | if (!super.equals(other)) return false
13 | val that = other as TokenExchangeGrantRequest
14 | return subjectToken == that.subjectToken
15 | }
16 |
17 | override fun hashCode() = Objects.hash(super.hashCode(), subjectToken)
18 | }
--------------------------------------------------------------------------------
/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/ClientAuthenticationPropertiesTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core
2 |
3 | import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod
4 | import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_JWT
5 | import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.NONE
6 | import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH
7 | import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.TLS_CLIENT_AUTH
8 | import no.nav.security.token.support.client.core.ClientAuthenticationProperties.Companion.builder
9 | import org.junit.jupiter.api.Test
10 | import org.junit.jupiter.api.assertThrows
11 |
12 | internal class ClientAuthenticationPropertiesTest {
13 |
14 | @Test
15 | fun invalidAuthenticationProperties() {
16 | assertThrows { instanceWith(TLS_CLIENT_AUTH) }
17 | assertThrows { instanceWith(SELF_SIGNED_TLS_CLIENT_AUTH) }
18 | assertThrows { instanceWith(CLIENT_SECRET_JWT) }
19 | assertThrows { instanceWith(NONE) }
20 | assertThrows { builder("client1", NONE).build() }
21 | }
22 |
23 | private fun instanceWith(clientAuthenticationMethod : ClientAuthenticationMethod) =
24 | ClientAuthenticationProperties("client", clientAuthenticationMethod, "secret",
25 | null)
26 |
27 | }
--------------------------------------------------------------------------------
/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/auth/ClientAssertionTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.auth
2 |
3 | import com.nimbusds.jose.JOSEObjectType.JWT
4 | import com.nimbusds.jose.JWSAlgorithm.RS256
5 | import com.nimbusds.jose.crypto.RSASSAVerifier
6 | import com.nimbusds.jwt.SignedJWT
7 | import com.nimbusds.oauth2.sdk.GrantType.CLIENT_CREDENTIALS
8 | import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.PRIVATE_KEY_JWT
9 | import java.net.URI
10 | import java.time.Instant
11 | import java.util.*
12 | import no.nav.security.token.support.client.core.ClientAuthenticationProperties.Companion.builder
13 | import no.nav.security.token.support.client.core.ClientProperties.Companion.builder
14 | import org.assertj.core.api.Assertions.assertThat
15 | import org.junit.jupiter.api.Test
16 |
17 | internal class ClientAssertionTest {
18 |
19 | @Test
20 | fun testCreateAssertion() {
21 | val clientAuth = builder("client1", PRIVATE_KEY_JWT).clientJwk("src/test/resources/jwk.json").build()
22 | val p = builder(CLIENT_CREDENTIALS, clientAuth).tokenEndpointUrl(URI.create("http://token")).build()
23 | val signedJWT = SignedJWT.parse(ClientAssertion(p.tokenEndpointUrl!!, p.authentication).assertion())
24 | assertThat(signedJWT.header.keyID).isEqualTo(p.authentication.clientRsaKey?.keyID)
25 | assertThat(signedJWT.header.type).isEqualTo(JWT)
26 | assertThat(signedJWT.header.algorithm).isEqualTo(RS256)
27 | assertThat(signedJWT.verify(RSASSAVerifier(clientAuth.clientRsaKey))).isTrue()
28 | val claims = signedJWT.jwtClaimsSet
29 | assertThat(claims.subject).isEqualTo(clientAuth.clientId)
30 | assertThat(claims.issuer).isEqualTo(clientAuth.clientId)
31 | assertThat(claims.audience).containsExactly(p.tokenEndpointUrl.toString())
32 | assertThat(claims.expirationTime).isAfter(Date.from(Instant.now()))
33 | assertThat(claims.notBeforeTime).isBefore(claims.expirationTime)
34 | }
35 | }
--------------------------------------------------------------------------------
/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.http
2 |
3 | import no.nav.security.token.support.client.core.http.OAuth2HttpHeaders.Companion.builder
4 | import no.nav.security.token.support.client.core.http.OAuth2HttpHeaders.Companion.of
5 | import org.assertj.core.api.Assertions.assertThat
6 | import org.junit.jupiter.api.Test
7 |
8 | internal class OAuth2HttpHeadersTest {
9 |
10 | @Test
11 | fun test() {
12 | val httpHeadersFromBuilder = builder()
13 | .header("header1", "header1value1")
14 | .header("header1", "header1value2")
15 | .build()
16 | val httpHeadersFromOf = of(mutableMapOf("header1" to listOf("header1value1", "header1value2")))
17 | assertThat(httpHeadersFromBuilder).isEqualTo(httpHeadersFromOf)
18 | assertThat(httpHeadersFromBuilder.headers).hasSize(1)
19 | assertThat(httpHeadersFromBuilder.headers).isEqualTo(httpHeadersFromOf.headers)
20 | }
21 | }
--------------------------------------------------------------------------------
/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/jwk/JwkFactoryTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.core.jwk
2 |
3 | import com.nimbusds.jose.util.Base64URL.encode
4 | import java.security.KeyStore
5 | import java.security.MessageDigest.getInstance
6 | import no.nav.security.token.support.client.core.jwk.JwkFactory.fromJsonFile
7 | import no.nav.security.token.support.client.core.jwk.JwkFactory.fromKeyStore
8 | import org.assertj.core.api.Assertions.assertThat
9 | import org.junit.jupiter.api.Test
10 |
11 | internal class JwkFactoryTest {
12 |
13 | @Test
14 | fun keyFromJwkFile() {
15 | val rsaKey = fromJsonFile("src/test/resources/jwk.json")
16 | assertThat(rsaKey.keyID).isEqualTo("jlAX4HYKW4hyhZgSmUyOmVAqMUw")
17 | assertThat(rsaKey.isPrivate).isTrue()
18 | assertThat(rsaKey.privateExponent).hasToString("J_mMSpq8k4WH9GKeS6d1kPVrQz2jDslAy3b3zrBuiSdNtKgUN7jFhGXaiY-cAg3efhMc-MWwPa0raKEN9xQRtIdbJurJbNG3viCvo_8FNs5lmFCUIktuO12zvsJS63q-i1zsZ7_esYQHbeDqg9S3q98c2EIO8lxQvPBcq-OIjdxfuanAEWJIRNuvNkK5I0AcqF_Q_KeFQDHo5sWUkwyPCaddd-ogS_YDeK3eeUpQbElrusdv0Ai0iYBPukzEHz1aL8PbaYru9f6Alor6yt9Lc_FNKfi-gnNFdpg3-uqVEh-MhEXgyN1RkeZzt0Kk9rylHumjSpwEgzuuA2L3WnycUQ")
19 | }
20 |
21 | @Test
22 | fun keyFromKeystore() {
23 | val rsaKey = fromKeyStore(ALIAS, inputStream(KEY_STORE_FILE), "Test1234")
24 | assertThat(rsaKey.keyID).isEqualTo(certificateThumbprintSHA1())
25 | assertThat(rsaKey.isPrivate).isTrue()
26 | }
27 |
28 | companion object {
29 |
30 | private const val KEY_STORE_FILE = "/selfsigned.jks"
31 | private const val ALIAS = "client_assertion"
32 | private fun certificateThumbprintSHA1() : String {
33 | return try {
34 | val keyStore = KeyStore.getInstance("JKS").apply {
35 | load(inputStream(KEY_STORE_FILE), "Test1234".toCharArray())
36 | }
37 | "${encode(getInstance("SHA-1").digest(keyStore.getCertificate(ALIAS).encoded))}"
38 | }
39 | catch (e : Exception) {
40 | throw RuntimeException(e)
41 | }
42 | }
43 |
44 | private fun inputStream(resource : String) = JwkFactoryTest::class.java.getResourceAsStream(resource) ?: throw IllegalArgumentException("resource not found: $resource")
45 |
46 | }
47 | }
--------------------------------------------------------------------------------
/token-client-core/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | %d{yyyy-MM-dd HH:mm:ss} %X{X-Nav-CallId} [%thread] %-5level %logger{70} - %msg%n
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/token-client-core/src/test/resources/selfsigned.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/navikt/token-support/731015c17f10aa7d9286dea5b0480c1837d7944e/token-client-core/src/test/resources/selfsigned.jks
--------------------------------------------------------------------------------
/token-client-kotlin-demo/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 |
12 | ### IntelliJ IDEA ###
13 | .idea
14 | *.iws
15 | *.iml
16 | *.ipr
17 |
18 | ### NetBeans ###
19 | nbproject/private/
20 | build/
21 | nbbuild/
22 | dist/
23 | nbdist/
24 | .nb-gradle/
25 |
--------------------------------------------------------------------------------
/token-client-kotlin-demo/src/main/kotlin/no/nav/security/token/support/ktor/oauth/ClientConfig.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.ktor.oauth
2 |
3 | import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod
4 | import io.ktor.client.*
5 | import io.ktor.server.config.*
6 | import no.nav.security.token.support.client.core.ClientAuthenticationProperties
7 |
8 | class ClientConfig(applicationConfig: ApplicationConfig, httpClient: HttpClient) {
9 | private val cacheConfig =
10 | with(applicationConfig.config(CACHE_PATH)) {
11 | OAuth2CacheConfig(propertyToStringOrNull("cache.enabled")?.toBoolean() ?: false, propertyToStringOrNull("cache.maximumSize")?.toLong() ?: 0, propertyToStringOrNull("cache.evictSkew")?.toLong() ?: 0)
12 | }
13 |
14 | internal val clients =
15 | applicationConfig.configList(CLIENTS_PATH)
16 | .associate {
17 | val wellKnownUrl = it.propertyToString("well_known_url")
18 | val clientAuth = ClientAuthenticationProperties(
19 | it.propertyToString("authentication.client_id"),
20 | ClientAuthenticationMethod(it.propertyToString("authentication.client_auth_method")),
21 | it.propertyToStringOrNull("client_secret"),
22 | it.propertyToStringOrNull("authentication.client_jwk"))
23 | it.propertyToString(CLIENT_NAME) to OAuth2Client(httpClient, wellKnownUrl, clientAuth, cacheConfig)
24 | }
25 |
26 | companion object CommonConfigurationAttributes {
27 | const val COMMON_PREFIX = "no.nav.security.jwt.client.registration"
28 | const val CLIENTS_PATH = "${COMMON_PREFIX}.clients"
29 | const val CACHE_PATH = "${COMMON_PREFIX}.cache"
30 | const val CLIENT_NAME = "client_name"
31 | }
32 | }
33 |
34 | internal fun ApplicationConfig.propertyToString(prop: String) = property(prop).getString()
35 | internal fun ApplicationConfig.propertyToStringOrNull(prop: String) = propertyOrNull(prop)?.getString()
--------------------------------------------------------------------------------
/token-client-kotlin-demo/src/main/kotlin/no/nav/security/token/support/ktor/oauth/OAuth2Cache.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.ktor.oauth
2 |
3 | import com.github.benmanes.caffeine.cache.AsyncLoadingCache
4 | import com.github.benmanes.caffeine.cache.Caffeine
5 | import com.github.benmanes.caffeine.cache.Expiry
6 | import java.util.concurrent.TimeUnit
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.future.future
9 | import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse
10 |
11 | data class OAuth2CacheConfig(val enabled: Boolean, val maximumSize: Long = 1000, val evictSkew: Long = 5) {
12 | fun cache(cacheContext: CoroutineScope, loader: suspend (GrantRequest) -> OAuth2AccessTokenResponse): AsyncLoadingCache =
13 | Caffeine.newBuilder()
14 | .expireAfter(evictOnResponseExpiresIn(evictSkew))
15 | .maximumSize(maximumSize)
16 | .buildAsync { key, _ ->
17 | cacheContext.future {
18 | loader(key)
19 | }
20 | }
21 |
22 | private fun evictOnResponseExpiresIn(skewInSeconds: Long): Expiry {
23 | return object : Expiry {
24 |
25 | override fun expireAfterCreate(key: GrantRequest, response: OAuth2AccessTokenResponse, currentTime: Long): Long {
26 | val seconds =
27 | if (response.expires_in!! > skewInSeconds) response.expires_in!! - skewInSeconds else response.expires_in!!
28 | .toLong()
29 | return TimeUnit.SECONDS.toNanos(seconds)
30 | }
31 |
32 | override fun expireAfterUpdate(key: GrantRequest, response: OAuth2AccessTokenResponse, currentTime: Long, currentDuration: Long): Long = currentDuration
33 |
34 |
35 | override fun expireAfterRead(key: GrantRequest, response: OAuth2AccessTokenResponse, currentTime: Long, currentDuration: Long): Long = currentDuration
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/token-client-kotlin-demo/src/main/resources/application.conf:
--------------------------------------------------------------------------------
1 | ktor {
2 | deployment {
3 | port = 8085
4 | port = ${?PORT}
5 | }
6 | application {
7 | modules = [no.nav.security.token.support.ktor.ApplicationKt.module]
8 | }
9 | }
10 |
11 | no.nav.security.jwt.client.registration {
12 | clients = [
13 | {
14 | client_name = "issuer1"
15 | well_known_url = "http://localhost:1111/issuer1/.well-known/oauth-authorization-server"
16 | authentication = {
17 | client_id = some-random-id
18 | client_auth_method = private_key_jwt
19 | client_jwk = src/main/resources/jwk.json
20 | }
21 | }
22 | ]
23 | cache = {
24 | enabled = true
25 | maximumSize = 1000
26 | evictSkew = 5
27 | }
28 | }
29 |
30 | no.nav.security.jwt {
31 | issuers = [
32 | {
33 | issuer_name = someshortname
34 | discoveryurl = "http://localhost:1111/default/.well-known/oauth-authorization-server"
35 | accepted_audience = debugger
36 | }
37 | ]
38 | }
--------------------------------------------------------------------------------
/token-client-kotlin-demo/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/token-client-spring-demo/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 |
12 | ### IntelliJ IDEA ###
13 | .idea
14 | *.iws
15 | *.iml
16 | *.ipr
17 |
18 | ### NetBeans ###
19 | /nbproject/private/
20 | /build/
21 | /nbbuild/
22 | /dist/
23 | /nbdist/
24 | /.nb-gradle/
--------------------------------------------------------------------------------
/token-client-spring-demo/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/navikt/token-support/731015c17f10aa7d9286dea5b0480c1837d7944e/token-client-spring-demo/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/token-client-spring-demo/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip
2 |
--------------------------------------------------------------------------------
/token-client-spring-demo/src/main/kotlin/no/nav/security/token/support/demo/spring/DemoApplication.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.demo.spring
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class DemoApplication
8 |
9 | fun main(args : Array) {
10 | runApplication(*args) {
11 | setAdditionalProfiles("mock")
12 | }
13 | }
--------------------------------------------------------------------------------
/token-client-spring-demo/src/main/kotlin/no/nav/security/token/support/demo/spring/client/DemoClient1.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.demo.spring.client
2 |
3 | import org.springframework.beans.factory.annotation.Value
4 | import org.springframework.stereotype.Service
5 | import org.springframework.web.client.RestClient.Builder
6 | import org.springframework.web.client.body
7 |
8 | @Service
9 | class DemoClient1(@Value("\${democlient1.url}") url : String, builder : Builder) {
10 |
11 | private val client = builder.baseUrl(url).build()
12 | fun ping() = client.get()
13 | .uri { b -> b.path("/ping").build() }
14 | .retrieve()
15 | .body()
16 | }
--------------------------------------------------------------------------------
/token-client-spring-demo/src/main/kotlin/no/nav/security/token/support/demo/spring/client/DemoClient2.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.demo.spring.client
2 |
3 | import org.springframework.beans.factory.annotation.Value
4 | import org.springframework.stereotype.Service
5 | import org.springframework.web.client.RestClient.Builder
6 | import org.springframework.web.client.body
7 |
8 | @Service
9 | class DemoClient2(@Value("\${democlient2.url}") url : String,builder : Builder) {
10 |
11 | private val client = builder.baseUrl(url).build()
12 | fun ping() = client.get()
13 | .uri { b -> b.path("/ping").build() }
14 | .retrieve()
15 | .body()
16 | }
--------------------------------------------------------------------------------
/token-client-spring-demo/src/main/kotlin/no/nav/security/token/support/demo/spring/client/DemoClient3.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.demo.spring.client
2 |
3 | import org.springframework.beans.factory.annotation.Value
4 | import org.springframework.stereotype.Service
5 | import org.springframework.web.client.RestClient.Builder
6 | import org.springframework.web.client.body
7 |
8 | @Service
9 | class DemoClient3(@Value("\${democlient3.url}") url : String, builder : Builder) {
10 |
11 | private val client = builder.baseUrl(url).build()
12 | fun ping() = client.get()
13 | .uri { b -> b.path("/ping").build() }
14 | .retrieve()
15 | .body()
16 | }
--------------------------------------------------------------------------------
/token-client-spring-demo/src/main/kotlin/no/nav/security/token/support/demo/spring/config/DemoConfiguration.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.demo.spring.config
2 |
3 | import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenService
4 | import no.nav.security.token.support.client.spring.ClientConfigurationProperties
5 | import no.nav.security.token.support.client.spring.oauth2.ClientConfigurationPropertiesMatcher
6 | import no.nav.security.token.support.client.spring.oauth2.EnableOAuth2Client
7 | import no.nav.security.token.support.client.spring.oauth2.OAuth2ClientRequestInterceptor
8 | import no.nav.security.token.support.spring.api.EnableJwtTokenValidation
9 | import org.springframework.boot.web.client.RestClientCustomizer
10 | import org.springframework.context.annotation.Bean
11 | import org.springframework.context.annotation.Configuration
12 |
13 | /***
14 | * You may only need one rest client if the short name in the config matches the canonical
15 | * hostname of the remote service. If not, you will need one rest client per remote service.
16 | * The rest client is configured with a base url, and the rest client customizer is used to register
17 | * a filter that will exchange add the access token to the request.
18 | *
19 | */
20 | @EnableOAuth2Client(cacheEnabled = true)
21 | @EnableJwtTokenValidation
22 | @Configuration
23 | class DemoConfiguration {
24 | @Bean
25 | fun customizer(reqInterceptor : OAuth2ClientRequestInterceptor) = RestClientCustomizer { it.requestInterceptor(reqInterceptor) }
26 |
27 | @Bean
28 | fun requestInterceptor(properties : ClientConfigurationProperties, service : OAuth2AccessTokenService, matcher : ClientConfigurationPropertiesMatcher) = OAuth2ClientRequestInterceptor(properties, service, matcher)
29 |
30 | @Bean
31 | fun configMatcher() = object: ClientConfigurationPropertiesMatcher{}
32 | }
--------------------------------------------------------------------------------
/token-client-spring-demo/src/main/kotlin/no/nav/security/token/support/demo/spring/rest/DemoController.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.demo.spring.rest
2 |
3 | import no.nav.security.token.support.core.api.Protected
4 | import no.nav.security.token.support.core.api.Unprotected
5 | import no.nav.security.token.support.demo.spring.client.DemoClient1
6 | import no.nav.security.token.support.demo.spring.client.DemoClient2
7 | import no.nav.security.token.support.demo.spring.client.DemoClient3
8 | import org.springframework.web.bind.annotation.GetMapping
9 | import org.springframework.web.bind.annotation.RestController
10 |
11 | @Protected
12 | @RestController
13 | class DemoController(private val demoClient1 : DemoClient1, private val demoClient2 : DemoClient2, private val demoClient3 : DemoClient3) {
14 |
15 | @GetMapping("/protected")
16 | fun protectedPath() = "i am protected"
17 |
18 | @Unprotected
19 | @GetMapping("/unprotected")
20 | fun unprotectedPath() = "i am unprotected"
21 |
22 | @Unprotected
23 | @GetMapping("/unprotected/client_credentials")
24 | fun pingWithClientCredentials() = demoClient1.ping()
25 |
26 | @GetMapping("/protected/on_behalf_of")
27 | fun pingWithOnBehalfOf() = demoClient2.ping()
28 |
29 | @GetMapping("/protected/exchange")
30 | fun pingExchange() = demoClient3.ping()
31 | }
--------------------------------------------------------------------------------
/token-client-spring-demo/src/main/resources/application.yaml:
--------------------------------------------------------------------------------
1 | no.nav.security.jwt:
2 |
3 | issuer:
4 | someshortname:
5 | discovery-url: http://metadata
6 | accepted_audience: aud-localhost
7 |
8 | client:
9 | registration:
10 | demoserver1:
11 | token-endpoint-url: http://localhost:8181/oauth2/v2.0/token
12 | grant-type: client_credentials
13 | scope: scope3, scope4
14 | authentication:
15 | client-id: testclient
16 | client-jwk: token-client-spring-demo/src/main/resources/jwk.json
17 | client-auth-method: private_key_jwt
18 |
19 | demoserver2:
20 | token-endpoint-url: http://localhost:8181/oauth2/v2.0/token
21 | grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer
22 | scope: scope1, scope2
23 | authentication:
24 | client-id: testclient
25 | client-secret: testsecret
26 | client-auth-method: client_secret_basic
27 |
28 | demoserver3:
29 | token-endpoint-url: http://localhost:8181/oauth2/v2.0/token
30 | grant-type: urn:ietf:params:oauth:grant-type:token-exchange
31 | authentication:
32 | client-id: cluster:namespace:app1
33 | client-jwk: token-client-spring-demo/src/main/resources/jwk.json
34 | client-auth-method: private_key_jwt
35 | token-exchange:
36 | audience: cluster:namespace:app2
37 |
38 |
39 | democlient1.url: http://demoserver1:8181
40 | democlient2.url: http://demoserver2:8181
41 | democlient3.url: http://demoserver2:8181
42 |
43 | mockwebserver:
44 | port: 8181
--------------------------------------------------------------------------------
/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/ClientConfigurationProperties.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.spring
2 |
3 | import jakarta.validation.Valid
4 | import jakarta.validation.constraints.NotEmpty
5 | import no.nav.security.token.support.client.core.ClientProperties
6 | import org.springframework.boot.context.properties.ConfigurationProperties
7 | import org.springframework.validation.annotation.Validated
8 |
9 | @Validated
10 | @ConfigurationProperties("no.nav.security.jwt.client")
11 | data class ClientConfigurationProperties(val registration: @NotEmpty @Valid Map)
--------------------------------------------------------------------------------
/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/ClientConfigurationPropertiesMatcher.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.spring.oauth2
2 |
3 | import java.net.URI
4 | import java.net.URI.create
5 | import no.nav.security.token.support.client.spring.ClientConfigurationProperties
6 |
7 | /**
8 | *
9 | * Default implementation that matcher host in request URL with the registration
10 | * name. Override for other strategies. Will typically be used with
11 | * [OAuth2ClientRequestInterceptor]. Must be registered by the
12 | * applications themselves, no automatic bean registration
13 | *
14 | */
15 | interface ClientConfigurationPropertiesMatcher {
16 |
17 | fun findProperties(properties: ClientConfigurationProperties, uri: String) = findProperties(properties, create(uri))
18 |
19 | fun findProperties(properties: ClientConfigurationProperties, uri: URI) =
20 | uri.host.split(".").firstOrNull()?.let {
21 | properties.registration[it]
22 | }
23 | }
--------------------------------------------------------------------------------
/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/DefaultOAuth2HttpClient.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.spring.oauth2
2 |
3 | import no.nav.security.token.support.client.core.OAuth2ClientException
4 | import no.nav.security.token.support.client.core.http.OAuth2HttpClient
5 | import no.nav.security.token.support.client.core.http.OAuth2HttpRequest
6 | import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse
7 | import org.springframework.http.HttpHeaders
8 | import org.springframework.util.LinkedMultiValueMap
9 | import org.springframework.web.client.RestClient
10 | import org.springframework.web.client.body
11 |
12 | open class DefaultOAuth2HttpClient : OAuth2HttpClient {
13 |
14 | val restClient = RestClient.create()
15 |
16 | override fun post(req: OAuth2HttpRequest) =
17 | restClient.post()
18 | .uri(req.tokenEndpointUrl)
19 | .headers { it.addAll(headers(req)) }
20 | .body(LinkedMultiValueMap().apply {
21 | setAll(req.formParameters)
22 | }).retrieve()
23 | .onStatus({ it.isError }) { _, response ->
24 | throw OAuth2ClientException("Received ${response.statusCode} from ${req.tokenEndpointUrl}")
25 | }
26 | .body() ?: throw OAuth2ClientException("No body in response from ${req.tokenEndpointUrl}")
27 |
28 | private fun headers(req: OAuth2HttpRequest): HttpHeaders = HttpHeaders().apply { putAll(req.oAuth2HttpHeaders.headers) }
29 |
30 | override fun toString() = "${javaClass.simpleName} [restClient=$restClient]"
31 | }
--------------------------------------------------------------------------------
/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/EnableOAuth2Client.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.spring.oauth2
2 |
3 | import java.lang.annotation.Inherited
4 | import org.springframework.context.annotation.Import
5 | import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
6 | import kotlin.annotation.AnnotationTarget.CLASS
7 |
8 | /**
9 | * Enables OAuth 2.0 clients for retrieving accesstokens using the
10 | * *client_credentials* and *on-behalf-of* flows.
11 | */
12 | @MustBeDocumented
13 | @Inherited
14 | @Retention(AnnotationRetention.RUNTIME)
15 | @Target(ANNOTATION_CLASS, CLASS)
16 | @Import(OAuth2ClientConfiguration::class)
17 | annotation class EnableOAuth2Client(
18 | /**
19 | * Enable caching for OAuth 2.0 access_token response in the [OAuth2AccessTokenService]
20 | * @return default value false, true if enabled
21 | */
22 | val cacheEnabled: Boolean = false,
23 | /**
24 | * Set the maximum cache size
25 | * @return the maximum entries in each cache instance
26 | */
27 | val cacheMaximumSize: Long = 1000,
28 | /**
29 | * Set skew time in seconds for cache eviction, i.e. the amount of time a cache entry
30 | * should be evicted before the actual "expires_in" in [OAuth2AccessTokenResponse]
31 | * @return the skew in seconds
32 | */
33 | val cacheEvictSkew: Long = 10)
--------------------------------------------------------------------------------
/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientRequestInterceptor.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.spring.oauth2
2 |
3 | import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenService
4 | import no.nav.security.token.support.client.spring.ClientConfigurationProperties
5 | import org.slf4j.LoggerFactory
6 | import org.springframework.http.HttpRequest
7 | import org.springframework.http.client.ClientHttpRequestExecution
8 | import org.springframework.http.client.ClientHttpRequestInterceptor
9 | import org.springframework.http.client.ClientHttpResponse
10 |
11 | /**
12 | *
13 | * Interceptor that exchanges a token using the [OAuth2AccessTokenService]
14 | * and sets the Authorization header to this new token, where the aud claim is set
15 | * to the destination app. The configuration fo this app is retrieved through a
16 | * configurable matcher implementing
17 | * [ClientConfigurationPropertiesMatcher]. If no configuration is found,
18 | * the interceptor is NOOP. The same applies if there is no Authorization header present, as there will be nothing to exchange in that case.
19 | * This again means that the interceptor can be safely registered on clients used for unauthenticated calls, such as pings/healthchecks.
20 | * This intercptor must be registered by the applications themselves, there is no automatic bean registration.
21 | *
22 | */
23 | class OAuth2ClientRequestInterceptor(private val properties: ClientConfigurationProperties,
24 | private val service: OAuth2AccessTokenService,
25 | private val matcher: ClientConfigurationPropertiesMatcher = object : ClientConfigurationPropertiesMatcher {}) : ClientHttpRequestInterceptor {
26 |
27 | private val log = LoggerFactory.getLogger(OAuth2ClientRequestInterceptor::class.java)
28 |
29 |
30 | override fun intercept(req: HttpRequest, body: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse {
31 | log.trace("Intercepting request to {}", req.uri)
32 | matcher.findProperties(properties, req.uri)?.let {
33 | log.trace("Found properties for uri {}", req.uri)
34 | service.getAccessToken(it).access_token?.let {
35 | token -> req.headers.setBearerAuth(token)
36 | log.trace("Finished setting access token in authorization header OK for uri {}", req.uri)
37 | }
38 | }
39 | return execution.execute(req, body)
40 | }
41 | override fun toString() = "${javaClass.simpleName} [properties=$properties, service=$service, matcher=$matcher]"
42 |
43 | }
--------------------------------------------------------------------------------
/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/ClientConfigurationPropertiesTestWithResourceUrl.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.spring.oauth2
2 |
3 |
4 | import no.nav.security.token.support.client.spring.ClientConfigurationProperties
5 | import no.nav.security.token.support.core.context.TokenValidationContextHolder
6 | import org.assertj.core.api.Assertions.assertThat
7 | import org.junit.jupiter.api.Test
8 | import org.junit.jupiter.api.extension.ExtendWith
9 | import org.mockito.junit.jupiter.MockitoExtension
10 | import org.springframework.beans.factory.annotation.Autowired
11 | import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration
12 | import org.springframework.boot.test.context.SpringBootTest
13 | import org.springframework.test.context.ActiveProfiles
14 | import org.springframework.test.context.bean.override.mockito.MockitoBean
15 |
16 | @SpringBootTest(classes = [OAuth2ClientConfiguration::class, RestClientAutoConfiguration::class])
17 | @ExtendWith(MockitoExtension::class)
18 | @ActiveProfiles("test-withresourceurl")
19 | internal class ClientConfigurationPropertiesTestWithResourceUrl {
20 |
21 | private val matcher = object: ClientConfigurationPropertiesMatcher {}
22 | @MockitoBean
23 | private val tokenValidationContextHolder: TokenValidationContextHolder? = null
24 |
25 | @Autowired
26 | private lateinit var clientConfigurationProperties: ClientConfigurationProperties
27 |
28 | @Test
29 | fun testClientConfigIsValid() {
30 | assertThat(matcher.findProperties(clientConfigurationProperties, "https://isdialogmelding.dev.intern.nav.no/api/person/v1/behandler/self")).isNotNull
31 | assertThat(clientConfigurationProperties).isNotNull
32 | val clientProperties = clientConfigurationProperties.registration.values.firstOrNull()
33 | assertThat(clientProperties).isNotNull
34 | val auth = clientProperties?.authentication
35 | assertThat(auth?.clientId).isNotNull
36 | assertThat(auth?.clientSecret).isNotNull
37 | assertThat(clientProperties?.scope).isNotEmpty
38 | assertThat(clientProperties?.tokenEndpointUrl).isNotNull
39 | assertThat(clientProperties?.grantType?.value).isNotNull
40 | assertThat(clientProperties?.resourceUrl).isNotNull
41 | }
42 | }
--------------------------------------------------------------------------------
/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientConfigurationWithCacheTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.spring.oauth2
2 |
3 | import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenService
4 | import no.nav.security.token.support.core.context.TokenValidationContextHolder
5 | import org.assertj.core.api.Assertions.assertThat
6 | import org.junit.jupiter.api.Test
7 | import org.springframework.beans.factory.annotation.Autowired
8 | import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration
9 | import org.springframework.boot.test.context.SpringBootTest
10 | import org.springframework.context.annotation.Configuration
11 | import org.springframework.test.context.ActiveProfiles
12 | import org.springframework.test.context.bean.override.mockito.MockitoBean
13 |
14 | @SpringBootTest(classes = [ConfigurationWithCacheEnabledTrue::class, RestClientAutoConfiguration::class])
15 | @ActiveProfiles("test")
16 | internal class OAuth2ClientConfigurationWithCacheTest {
17 |
18 | @MockitoBean
19 | private val tokenValidationContextHolder: TokenValidationContextHolder? = null
20 |
21 | @Autowired
22 | private lateinit var oAuth2AccessTokenService: OAuth2AccessTokenService
23 |
24 | @Test
25 | fun oAuth2AccessTokenServiceCreatedWithCache() {
26 | assertThat(oAuth2AccessTokenService).isNotNull
27 | assertThat(oAuth2AccessTokenService.clientCredentialsGrantCache).isNotNull
28 | assertThat(oAuth2AccessTokenService.onBehalfOfGrantCache).isNotNull
29 | assertThat(oAuth2AccessTokenService.exchangeGrantCache).isNotNull
30 | }
31 | }
32 |
33 | @Configuration
34 | @EnableOAuth2Client(cacheEnabled = true, cacheEvictSkew = 5, cacheMaximumSize = 100)
35 | internal class ConfigurationWithCacheEnabledTrue
--------------------------------------------------------------------------------
/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientConfigurationWithoutCacheTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.spring.oauth2
2 |
3 | import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenService
4 | import no.nav.security.token.support.core.context.TokenValidationContextHolder
5 | import org.assertj.core.api.Assertions.assertThat
6 | import org.junit.jupiter.api.Test
7 | import org.springframework.beans.factory.annotation.Autowired
8 | import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration
9 | import org.springframework.boot.test.context.SpringBootTest
10 | import org.springframework.context.annotation.Configuration
11 | import org.springframework.test.context.ActiveProfiles
12 | import org.springframework.test.context.bean.override.mockito.MockitoBean
13 |
14 | @SpringBootTest(classes = [ConfigurationWithCacheEnabledFalse::class, RestClientAutoConfiguration::class])
15 | @ActiveProfiles("test")
16 | internal class OAuth2ClientConfigurationWithoutCacheTest {
17 | @MockitoBean
18 | private val tokenValidationContextHolder: TokenValidationContextHolder? = null
19 |
20 | @Autowired
21 | private lateinit var oAuth2AccessTokenService: OAuth2AccessTokenService
22 |
23 | @Test
24 | fun oAuth2AccessTokenServiceCreatedWithoutCache() {
25 | assertThat(oAuth2AccessTokenService).isNotNull
26 | assertThat(oAuth2AccessTokenService.clientCredentialsGrantCache).isNull()
27 | assertThat(oAuth2AccessTokenService.onBehalfOfGrantCache).isNull()
28 | assertThat(oAuth2AccessTokenService.exchangeGrantCache).isNull()
29 | }
30 | }
31 |
32 | @Configuration
33 | @EnableOAuth2Client
34 | internal class ConfigurationWithCacheEnabledFalse
--------------------------------------------------------------------------------
/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/TestUtils.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.client.spring.oauth2
2 |
3 |
4 | import okhttp3.mockwebserver.MockResponse
5 | import org.springframework.http.HttpHeaders.CONTENT_TYPE
6 | import org.springframework.http.MediaType.APPLICATION_JSON_VALUE
7 |
8 | internal object TestUtils {
9 |
10 | fun jsonResponse(json: String)=
11 | MockResponse().apply {
12 | setHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
13 | setBody(json)
14 | }
15 | }
--------------------------------------------------------------------------------
/token-client-spring/src/test/resources/application-test-withresourceurl.yml:
--------------------------------------------------------------------------------
1 | no.nav.security.jwt.client:
2 | registration:
3 | example1-onbehalfof:
4 | resource-url: http://someapi
5 | token-endpoint-url: http://tokens.no
6 | grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer
7 | scope: scope1, scope2
8 | authentication:
9 | client-id: testclient
10 | client-secret: testsecret
11 | client-auth-method: client_secret_basic
12 |
13 | example1-onbehalfof2:
14 | token-endpoint-url: http://tokens.no
15 | grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer
16 | scope: scope3, scope4
17 | authentication:
18 | client-id: testclient
19 | client-secret: testsecret
20 | client-auth-method: client_secret_basic
21 |
--------------------------------------------------------------------------------
/token-client-spring/src/test/resources/application-test-withwellknownurl.yml:
--------------------------------------------------------------------------------
1 | no.nav.security.jwt.client:
2 | registration:
3 | example1-token-exchange1:
4 | well-known-url: http://localhost:${mockwebserver.port}/well-known
5 | grant-type: urn:ietf:params:oauth:grant-type:token-exchange
6 | authentication:
7 | client-id: cluster:namespace:app1
8 | client-jwk: src/test/resources/jwk.json
9 | client-auth-method: private_key_jwt
10 | token-exchange:
11 | audience: cluster:namespace:app2
12 |
13 | logging.level.okhttp3: DEBUG
14 |
--------------------------------------------------------------------------------
/token-client-spring/src/test/resources/application-test.yml:
--------------------------------------------------------------------------------
1 | no.nav.security.jwt.client:
2 | registration:
3 | example1-onbehalfof:
4 | token-endpoint-url: http://tokens.no
5 | grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer
6 | scope: scope1, scope2
7 | authentication:
8 | client-id: testclient
9 | client-secret: testsecret
10 | client-auth-method: client_secret_basic
11 |
12 | example1-onbehalfof2:
13 | token-endpoint-url: http://tokens.no
14 | grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer
15 | scope: scope3, scope4
16 | authentication:
17 | client-id: testclient
18 | client-secret: testsecret
19 | client-auth-method: client_secret_basic
20 |
21 | example1-clientcredentials1:
22 | token-endpoint-url: http://tokens.no
23 | grant-type: client_credentials
24 | scope: scope1, scope2
25 | authentication:
26 | client-id: testclient
27 | client-secret: testsecret
28 | client-auth-method: client_secret_basic
29 |
30 | example1-clientcredentials2:
31 | token-endpoint-url: http://tokens.no
32 | grant-type: client_credentials
33 | scope: scope3, scope4
34 | authentication:
35 | client-id: testclient
36 | client-secret: testsecret
37 | client-auth-method: client_secret_basic
38 |
39 | example1-clientcredentials3:
40 | token-endpoint-url: http://tokens.no
41 | grant-type: client_credentials
42 | scope: scope3, scope4
43 | authentication:
44 | client-id: testclient
45 | client-jwk: src/test/resources/jwk.json
46 | client-auth-method: private_key_jwt
47 |
48 | example1-token-exchange1:
49 | token-endpoint-url: http://tokens.no
50 | grant-type: urn:ietf:params:oauth:grant-type:token-exchange
51 | authentication:
52 | client-id: cluster:namespace:app1
53 | client-jwk: src/test/resources/jwk.json
54 | client-auth-method: private_key_jwt
55 | token-exchange:
56 | audience: cluster:namespace:app2
--------------------------------------------------------------------------------
/token-client-spring/src/test/resources/banner.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/navikt/token-support/731015c17f10aa7d9286dea5b0480c1837d7944e/token-client-spring/src/test/resources/banner.txt
--------------------------------------------------------------------------------
/token-validation-core/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 |
12 | ### IntelliJ IDEA ###
13 | .idea
14 | *.iws
15 | *.iml
16 | *.ipr
17 |
18 | ### NetBeans ###
19 | nbproject/private/
20 | build/
21 | nbbuild/
22 | dist/
23 | nbdist/
24 | .nb-gradle/
25 |
--------------------------------------------------------------------------------
/token-validation-core/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | no.nav.security
6 | token-support
7 | 3.0.0-SNAPSHOT
8 |
9 | token-validation-core
10 | token-validation-core
11 |
12 |
13 | com.nimbusds
14 | oauth2-oidc-sdk
15 |
16 |
17 | org.slf4j
18 | slf4j-api
19 |
20 |
21 | jakarta.validation
22 | jakarta.validation-api
23 |
24 |
25 | ch.qos.logback
26 | logback-classic
27 | test
28 |
29 |
30 | com.squareup.okhttp3
31 | mockwebserver
32 | test
33 |
34 |
35 |
36 | ${project.basedir}/src/main/kotlin
37 | ${project.basedir}/src/test/kotlin
38 |
39 |
40 | maven-surefire-plugin
41 |
42 |
43 | localhost,127.0.0.1,10.254.0.1,.local,.adeo.no,.nav.no,.aetat.no,.devillo.no,.oera.no,.nais.io,.aivencloud.com
44 |
45 |
46 |
47 |
48 | org.jetbrains.kotlin
49 | kotlin-maven-plugin
50 |
51 |
52 | org.apache.maven.plugins
53 | maven-source-plugin
54 |
55 |
56 |
57 |
58 |
59 | release
60 |
61 |
62 |
63 | org.apache.maven.plugins
64 | maven-gpg-plugin
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/JwtTokenConstants.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core
2 |
3 | object JwtTokenConstants {
4 | const val AUTHORIZATION_HEADER = "Authorization"
5 | const val EXPIRY_THRESHOLD_ENV_PROPERTY = "no.nav.security.jwt.expirythreshold"
6 | const val TOKEN_VALIDATION_FILTER_ORDER_PROPERTY = "no.nav.security.jwt.tokenvalidationfilter.order"
7 | const val TOKEN_EXPIRES_SOON_HEADER = "x-token-expires-soon"
8 | const val BEARER_TOKEN_DONT_PROPAGATE_ENV_PROPERTY = "no.nav.security.jwt.dont-propagate-bearertoken"
9 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/Protected.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.api
2 |
3 | import kotlin.annotation.AnnotationRetention.RUNTIME
4 | import kotlin.annotation.AnnotationTarget.CLASS
5 | import kotlin.annotation.AnnotationTarget.FUNCTION
6 | import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
7 | import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
8 |
9 | @Retention(RUNTIME)
10 | @MustBeDocumented
11 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, CLASS)
12 | annotation class Protected
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/ProtectedWithClaims.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.api
2 |
3 | import no.nav.security.token.support.core.utils.Cluster
4 | import kotlin.annotation.AnnotationRetention.RUNTIME
5 | import kotlin.annotation.AnnotationTarget.CLASS
6 | import kotlin.annotation.AnnotationTarget.FUNCTION
7 | import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
8 | import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
9 |
10 | @Retention(RUNTIME)
11 | @Target(CLASS, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
12 | @Protected
13 | @MustBeDocumented
14 | annotation class ProtectedWithClaims(val issuer : String,
15 | /**
16 | * Required claims in token in key=value format.
17 | * If the value is an asterisk (*), it checks that the required key is present.
18 | * @return array containing claims as key=value
19 | */
20 | val claimMap : Array = [], val excludedClusters : Array = [],
21 | /**
22 | * How to check for the presence of claims,
23 | * default is false which will require all claims in the list
24 | * to be present in token. If set to true, any claim in the list
25 | * will suffice.
26 | *
27 | * @return boolean
28 | */
29 | val combineWithOr : Boolean = false)
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/RequiredIssuers.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.api
2 |
3 | import kotlin.annotation.AnnotationRetention.RUNTIME
4 |
5 | @Retention(RUNTIME)
6 | @MustBeDocumented
7 | annotation class RequiredIssuers(vararg val value : ProtectedWithClaims)
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/Unprotected.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.api
2 |
3 | import kotlin.annotation.AnnotationRetention.RUNTIME
4 | import kotlin.annotation.AnnotationTarget.CLASS
5 | import kotlin.annotation.AnnotationTarget.FUNCTION
6 | import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
7 | import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
8 |
9 | @Retention(RUNTIME)
10 | @MustBeDocumented
11 | @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, CLASS)
12 | annotation class Unprotected
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/IssuerConfiguration.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.configuration
2 |
3 | import com.nimbusds.jose.util.ResourceRetriever
4 | import com.nimbusds.oauth2.sdk.`as`.AuthorizationServerMetadata
5 | import java.net.URL
6 | import no.nav.security.token.support.core.exceptions.MetaDataNotAvailableException
7 | import no.nav.security.token.support.core.validation.JwtTokenValidator
8 | import no.nav.security.token.support.core.validation.JwtTokenValidatorFactory.tokenValidator
9 |
10 | open class IssuerConfiguration(val name : String, properties : IssuerProperties, val resourceRetriever : ResourceRetriever = ProxyAwareResourceRetriever()) {
11 |
12 | val metadata : AuthorizationServerMetadata
13 | val acceptedAudience = properties.acceptedAudience
14 | val headerName = properties.headerName
15 | val tokenValidator : JwtTokenValidator
16 |
17 | init {
18 | metadata = providerMetadata(resourceRetriever, properties.discoveryUrl)
19 | tokenValidator = tokenValidator(properties, metadata, resourceRetriever)
20 | }
21 |
22 | override fun toString() = ("${javaClass.simpleName} [name=$name, metaData=$metadata, acceptedAudience=$acceptedAudience, headerName=$headerName, tokenValidator=$tokenValidator, resourceRetriever=$resourceRetriever]")
23 |
24 | companion object {
25 |
26 | private fun providerMetadata(retriever : ResourceRetriever, url : URL) =
27 | runCatching {
28 | AuthorizationServerMetadata.parse(retriever.retrieveResource(url).content)
29 | }.getOrElse {
30 | throw MetaDataNotAvailableException("Make sure you are not using proxying in GCP", url, it)
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/MultiIssuerConfiguration.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.configuration
2 |
3 | import com.nimbusds.jose.util.ResourceRetriever
4 | import java.util.*
5 | import kotlin.DeprecationLevel.WARNING
6 |
7 | class MultiIssuerConfiguration @JvmOverloads constructor(private val properties : Map, val retriever : ResourceRetriever = ProxyAwareResourceRetriever()) {
8 |
9 | val issuerShortNames = ArrayList()
10 |
11 | val issuers : MutableMap = HashMap()
12 |
13 | init {
14 | loadIssuerConfigurations()
15 | }
16 | @Deprecated(message ="Use of Optional not necessary",ReplaceWith("getIssuers.get()"), WARNING)
17 | fun getIssuer(name : String) = Optional.ofNullable(issuers[name])
18 |
19 | private fun loadIssuerConfigurations() =
20 | properties.forEach { (shortName, p) ->
21 | createIssuerConfiguration(shortName, p).run {
22 | issuerShortNames.add(shortName)
23 | issuers[shortName] = this
24 | issuers["${metadata.issuer}"] = this
25 | }
26 | }
27 |
28 | private fun createIssuerConfiguration(shortName : String, p : IssuerProperties) =
29 | if (p.usePlaintextForHttps || p.proxyUrl != null) {
30 | IssuerConfiguration(shortName, p, ProxyAwareResourceRetriever(p.proxyUrl, p.usePlaintextForHttps))
31 | }
32 | else IssuerConfiguration(shortName, p, retriever)
33 |
34 | override fun toString() = ("${javaClass.simpleName} [issuerShortNames=$issuerShortNames, resourceRetriever=$retriever, issuers=$issuers, issuerPropertiesMap=$properties]")
35 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/context/TokenValidationContext.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.context
2 |
3 | import java.util.*
4 | import no.nav.security.token.support.core.jwt.JwtToken
5 |
6 | class TokenValidationContext(private val validatedTokens : Map) {
7 |
8 | fun getJwtTokenAsOptional(issuerName : String) = jwtToken(issuerName)?.let { Optional.of(it) } ?: Optional.empty()
9 |
10 | val firstValidToken get() = validatedTokens.values.firstOrNull()
11 | fun getJwtToken(issuerName : String) = jwtToken(issuerName)
12 |
13 | fun getClaims(issuerName : String) = jwtToken(issuerName)?.jwtTokenClaims ?: throw IllegalArgumentException("No token found for issuer $issuerName")
14 |
15 | val anyValidClaims get() =
16 | validatedTokens.values
17 | .map(JwtToken::jwtTokenClaims)
18 | .firstOrNull()
19 |
20 |
21 | fun hasValidToken() = validatedTokens.isNotEmpty()
22 |
23 | fun hasTokenFor(issuerName : String) = getJwtToken(issuerName) != null
24 | val issuers get() = validatedTokens.keys.toList()
25 |
26 | private fun jwtToken(issuerName: String) = validatedTokens[issuerName]
27 |
28 | override fun toString() = "TokenValidationContext{issuers=${validatedTokens.keys}}"
29 |
30 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/context/TokenValidationContextHolder.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.context
2 |
3 | interface TokenValidationContextHolder {
4 |
5 | fun getTokenValidationContext() : TokenValidationContext
6 |
7 | fun setTokenValidationContext(tokenValidationContext: TokenValidationContext?)
8 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/AnnotationRequiredException.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.exceptions
2 |
3 | import java.lang.reflect.Method
4 | import no.nav.security.token.support.core.validation.JwtTokenAnnotationHandler.Companion.SUPPORTED_ANNOTATIONS
5 | class AnnotationRequiredException(message : String) : RuntimeException(message) {
6 | constructor(method : Method) : this("Server misconfigured - controller/method [${method.declaringClass.name}.${method.name}] not annotated with any of $SUPPORTED_ANNOTATIONS or added to ignore list")
7 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/IssuerConfigurationException.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.exceptions
2 |
3 | class IssuerConfigurationException @JvmOverloads constructor(message : String, cause : Throwable? = null) : RuntimeException(message, cause)
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/JwtTokenInvalidClaimException.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.exceptions
2 |
3 | import no.nav.security.token.support.core.api.ProtectedWithClaims
4 | import no.nav.security.token.support.core.api.RequiredIssuers
5 |
6 | class JwtTokenInvalidClaimException(message : String) : RuntimeException(message) {
7 | constructor(ann : RequiredIssuers) : this("Required claims not present in token for any of ${issuersAndClaims(ann)}")
8 |
9 | constructor(ann : ProtectedWithClaims) : this("Required claims not present in token. ${listOf(*ann.claimMap)}")
10 |
11 | companion object {
12 | private fun issuersAndClaims(ann: RequiredIssuers) = ann.value.associate { it.issuer to it.claimMap }
13 | }
14 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/JwtTokenMissingException.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.exceptions
2 |
3 | import no.nav.security.token.support.core.api.RequiredIssuers
4 |
5 | class JwtTokenMissingException @JvmOverloads constructor(message : String? = "No valid token found in validation context") : RuntimeException(message) {
6 | constructor(ann : RequiredIssuers) : this("No valid token found in validation context for any of the issuers ${ann.value.map { it.issuer }}")
7 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/JwtTokenValidatorException.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.exceptions
2 |
3 | import java.util.*
4 |
5 | class JwtTokenValidatorException @JvmOverloads constructor(msg : String? = null, val expiryDate : Date? = null, cause : Throwable? = null) : RuntimeException(msg, cause) {
6 | constructor(msg : String, cause : Throwable) : this(msg, null,cause)
7 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/MetaDataNotAvailableException.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.exceptions
2 |
3 | import java.net.URL
4 |
5 | class MetaDataNotAvailableException(msg : String, url : URL, e : Throwable) : RuntimeException("Could not retrieve metadata from $url. $msg", e)
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/http/HttpRequest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.http
2 |
3 | /***
4 | * Abstraction interface for an HTTP request to avoid dependencies on specific implementations such as HttpServletRequest etc.
5 | */
6 | interface HttpRequest {
7 | fun getHeader(headerName: String): String?
8 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/jwt/JwtToken.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.jwt
2 |
3 | import com.nimbusds.jwt.JWT
4 | import com.nimbusds.jwt.JWTParser
5 | import com.nimbusds.jwt.SignedJWT
6 | import kotlin.DeprecationLevel.WARNING
7 |
8 | open class JwtToken(val encodedToken : String, protected val jwt : JWT, val jwtTokenClaims : JwtTokenClaims) {
9 | constructor(encodedToken : String) : this(encodedToken, JWTParser.parse(encodedToken), JwtTokenClaims(JWTParser.parse(encodedToken).jwtClaimsSet))
10 |
11 | val jwtClaimsSet = jwt.jwtClaimsSet
12 |
13 | val subject = jwtTokenClaims.subject
14 |
15 | val issuer = jwtTokenClaims.issuer
16 |
17 | @Deprecated("Use getEncodedToken instead", ReplaceWith("getEncodedToken()"), WARNING)
18 | val tokenAsString = encodedToken
19 |
20 | fun asBearer() = "Bearer $encodedToken"
21 |
22 | fun containsClaim(name : String, value : String) = jwtTokenClaims.containsClaim(name, value)
23 |
24 | companion object {
25 | fun SignedJWT.asBearer() = "Bearer ${serialize()}"
26 | }
27 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/jwt/JwtTokenClaims.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.jwt
2 |
3 | import com.nimbusds.jwt.JWTClaimsSet
4 |
5 | class JwtTokenClaims(private val claimSet : JWTClaimsSet) {
6 |
7 | val issuer = claimSet.issuer
8 | val expirationTime = claimSet.expirationTime
9 | val subject = claimSet.subject
10 | val allClaims = claimSet.claims
11 |
12 |
13 | fun get(name : String) = claimSet.getClaim(name)
14 | fun getStringClaim(name : String) = runCatching { claimSet.getStringClaim(name) }.getOrElse { throw RuntimeException(it) }
15 | fun getAsList(name : String) = runCatching { claimSet.getStringListClaim(name) }.getOrElse { throw RuntimeException(it) }
16 |
17 | fun containsClaim(name: String?, value: String) =
18 | when (val claim = claimSet.getClaim(name)) {
19 | is String -> value == "*" || claim == value
20 | is Collection<*> -> value == "*" || value in claim
21 | else -> false
22 | }
23 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/utils/Cluster.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.utils
2 |
3 | import no.nav.security.token.support.core.utils.EnvUtil.NAIS_CLUSTER_NAME
4 |
5 | enum class Cluster(private val navn : String) {
6 | TEST(EnvUtil.TEST),
7 | LOCAL(EnvUtil.LOCAL),
8 | DEV_SBS(EnvUtil.DEV_SBS),
9 | DEV_FSS(EnvUtil.DEV_FSS),
10 | DEV_GCP(EnvUtil.DEV_GCP),
11 | PROD_GCP(EnvUtil.PROD_GCP),
12 | PROD_FSS(EnvUtil.PROD_FSS),
13 | PROD_SBS(EnvUtil.PROD_SBS);
14 |
15 | companion object {
16 |
17 | @JvmStatic
18 | fun currentCluster() = entries.firstOrNull { it.navn == cluster() } ?: LOCAL
19 |
20 | @JvmStatic
21 | val isProd = cluster() in listOf(EnvUtil.PROD_GCP, EnvUtil.PROD_FSS)
22 | private fun cluster() = System.getenv(NAIS_CLUSTER_NAME)
23 | }
24 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/utils/EnvUtil.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.utils
2 |
3 | object EnvUtil {
4 |
5 | const val FSS = "fss"
6 | const val SBS = "sbs"
7 | const val LOCAL = "local"
8 | const val GCP = "gcp"
9 | const val TEST = "test"
10 | const val DEV = "dev"
11 | const val PROD = "prod"
12 |
13 | @JvmField
14 | val DEV_GCP = "$DEV-$GCP"
15 |
16 | @JvmField
17 | val PROD_GCP = "$PROD-$GCP"
18 |
19 | @JvmField
20 | val PROD_SBS = "$PROD-$SBS"
21 |
22 | @JvmField
23 | val DEV_SBS = "$DEV-$SBS"
24 |
25 | @JvmField
26 | val PROD_FSS = "$PROD-$FSS"
27 |
28 | @JvmField
29 | val DEV_FSS = "$DEV-$FSS"
30 | const val NAIS_CLUSTER_NAME = "NAIS_CLUSTER_NAME"
31 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/utils/JwtTokenUtil.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.utils
2 |
3 | import no.nav.security.token.support.core.context.TokenValidationContextHolder
4 |
5 | object JwtTokenUtil {
6 |
7 | @JvmStatic
8 | fun contextHasValidToken(holder : TokenValidationContextHolder?) = context(holder).hasValidToken()
9 | @JvmStatic
10 | fun getJwtToken(issuer : String, holder : TokenValidationContextHolder?) = context(holder).getJwtTokenAsOptional(issuer)
11 |
12 | private fun context(holder : TokenValidationContextHolder?) = holder?.getTokenValidationContext() ?: throw IllegalStateException("TokenValidationContextHolder is null")
13 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/validation/DefaultJwtClaimsVerifier.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.validation
2 |
3 | import com.nimbusds.jose.proc.SecurityContext
4 | import com.nimbusds.jwt.JWTClaimsSet
5 | import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier
6 | import com.nimbusds.jwt.util.DateUtils.isBefore
7 | import com.nimbusds.openid.connect.sdk.validators.BadJWTExceptions.IAT_CLAIM_AHEAD_EXCEPTION
8 | import java.util.*
9 |
10 | /**
11 | * Extends [com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier] with a time check for the issued at ("iat") claim.
12 | * The claim is only checked if it exists in the given claim set.
13 | */
14 | class DefaultJwtClaimsVerifier(acceptedAudience : Set?, exactMatchClaims : JWTClaimsSet, requiredClaims : Set, prohibitedClaims : Set) : DefaultJWTClaimsVerifier(acceptedAudience, exactMatchClaims, requiredClaims, prohibitedClaims) {
15 |
16 | override fun verify(claimsSet: JWTClaimsSet, context: C?) =
17 | super.verify(claimsSet, context).also {
18 | claimsSet.issueTime?.let { iat ->
19 | if (!isBefore(iat, Date(), maxClockSkew.toLong())) {
20 | throw IAT_CLAIM_AHEAD_EXCEPTION
21 | }
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/validation/JwtTokenRetriever.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.validation
2 |
3 | import java.util.*
4 | import no.nav.security.token.support.core.configuration.MultiIssuerConfiguration
5 | import no.nav.security.token.support.core.http.HttpRequest
6 | import no.nav.security.token.support.core.jwt.JwtToken
7 | import org.slf4j.Logger
8 | import org.slf4j.LoggerFactory
9 |
10 | object JwtTokenRetriever {
11 |
12 | private val LOG : Logger = LoggerFactory.getLogger(JwtTokenRetriever::class.java)
13 | private const val BEARER = "Bearer"
14 |
15 | @JvmStatic
16 | fun retrieveUnvalidatedTokens(config: MultiIssuerConfiguration, request: HttpRequest) =
17 | getTokensFromHeader(config, request)
18 |
19 | private fun getTokensFromHeader(config: MultiIssuerConfiguration, request: HttpRequest): List = try {
20 | LOG.debug("Checking authorization header for tokens using config {}", config)
21 | val issuer = config.issuers.values.firstOrNull { request.getHeader(it.headerName) != null }.let { Optional.ofNullable(it) }
22 | if (issuer.isPresent) {
23 | val headerValues = request.getHeader(issuer.get().headerName)?.split(",") ?: emptyList()
24 | extractBearerTokens(headerValues)
25 | .map(::JwtToken)
26 | .filterNot { config.issuers[it.issuer] == null }
27 | } else {
28 | emptyList().also { LOG.debug("No tokens found in authorization header") }
29 | }
30 | } catch (e: Exception) {
31 | emptyList().also {
32 | LOG.warn("Received exception when attempting to extract and parse token from Authorization header", e)
33 | }
34 | }
35 |
36 | private fun extractBearerTokens(headerValues: List) =
37 | headerValues
38 | .map { it.split(" ") }
39 | .filter { it.size == 2 && it[0].equals(BEARER, ignoreCase = true) }
40 | .map { it[1].trim() }
41 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/validation/JwtTokenValidator.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.validation
2 |
3 | interface JwtTokenValidator {
4 |
5 | fun assertValidToken(tokenString : String)
6 | }
--------------------------------------------------------------------------------
/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/validation/JwtTokenValidatorFactory.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.validation
2 |
3 | import com.nimbusds.jose.jwk.source.JWKSource
4 | import com.nimbusds.jose.jwk.source.JWKSourceBuilder
5 | import com.nimbusds.jose.proc.SecurityContext
6 | import com.nimbusds.jose.util.ResourceRetriever
7 | import com.nimbusds.oauth2.sdk.`as`.AuthorizationServerMetadata
8 | import java.net.URL
9 | import no.nav.security.token.support.core.configuration.IssuerProperties
10 |
11 | object JwtTokenValidatorFactory {
12 |
13 | @JvmStatic
14 | fun tokenValidator(p : IssuerProperties, md : AuthorizationServerMetadata, retriever : ResourceRetriever) = tokenValidator(p, md, jwkSource(p, md.jwkSetURI.toURL(), retriever))
15 |
16 | @JvmStatic
17 | fun tokenValidator(p : IssuerProperties, md : AuthorizationServerMetadata, remoteJWKSet : JWKSource) =
18 | DefaultConfigurableJwtValidator(md.issuer.value, p.acceptedAudience, p.validation.optionalClaims, remoteJWKSet)
19 |
20 | private fun jwkSource(p: IssuerProperties, jwksUrl: URL, retriever: ResourceRetriever) =
21 | JWKSourceBuilder.create(jwksUrl, retriever).apply {
22 | if (p.jwksCache.isConfigured) {
23 | cache(p.jwksCache.lifespanMillis, p.jwksCache.refreshTimeMillis)
24 | }
25 | }.build()
26 | }
--------------------------------------------------------------------------------
/token-validation-core/src/test/kotlin/no/nav/security/token/support/core/configuration/ProxyAwareResourceRetrieverTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.configuration
2 | import java.net.URI
3 | import org.junit.jupiter.api.Assertions.assertEquals
4 | import org.junit.jupiter.api.Assertions.assertFalse
5 | import org.junit.jupiter.api.Assertions.assertTrue
6 | import org.junit.jupiter.api.Test
7 |
8 | internal class ProxyAwareResourceRetrieverTest {
9 |
10 | @Test
11 | fun testNoProxy() {
12 | ProxyAwareResourceRetriever(URI.create("http://proxy:8080").toURL()).run {
13 | assertTrue(shouldProxy(URI.create("http://www.vg.no").toURL()))
14 | assertFalse(shouldProxy(URI.create("http:/www.aetat.no").toURL()))
15 | }
16 | ProxyAwareResourceRetriever().run {
17 | assertFalse(shouldProxy(URI.create("http:/www.aetat.no").toURL()))
18 | assertFalse(shouldProxy(URI.create("http://www.vg.no").toURL()))
19 | }
20 | }
21 |
22 | @Test
23 | fun testUsePlainTextForHttps() {
24 | val resourceRetriever = ProxyAwareResourceRetriever(null, true)
25 | val url = URI.create("https://host.domain.no/somepath?foo=bar&bar=foo").toURL()
26 | val plain = resourceRetriever.urlWithPlainTextForHttps(url)
27 | assertEquals(plain.protocol, "http")
28 | assertEquals(plain.port, 443)
29 | }
30 | }
--------------------------------------------------------------------------------
/token-validation-core/src/test/kotlin/no/nav/security/token/support/core/context/TokenValidationContextTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.context
2 |
3 | import com.nimbusds.jwt.JWTClaimsSet.Builder
4 | import com.nimbusds.jwt.PlainJWT
5 | import java.util.concurrent.ConcurrentHashMap
6 | import no.nav.security.token.support.core.jwt.JwtToken
7 | import org.assertj.core.api.Assertions.assertThat
8 | import org.junit.jupiter.api.Test
9 |
10 | internal class TokenValidationContextTest {
11 |
12 | @Test
13 | fun firstValidToken() {
14 | val map : MutableMap = ConcurrentHashMap()
15 | val tokenValidationContext = TokenValidationContext(map)
16 | assertThat(tokenValidationContext.firstValidToken).isNull()
17 | assertThat(tokenValidationContext.hasValidToken()).isFalse()
18 |
19 | val jwtToken1 = jwtToken("https://one")
20 | val jwtToken2 = jwtToken("https://two")
21 | map["issuer2"] = jwtToken2
22 | map["issuer1"] = jwtToken1
23 |
24 | assertThat(tokenValidationContext.firstValidToken)?.isEqualTo(jwtToken1)
25 | }
26 |
27 | private fun jwtToken(issuer : String) = JwtToken(PlainJWT(Builder().issuer(issuer).subject("subject").build()).serialize())
28 | }
--------------------------------------------------------------------------------
/token-validation-core/src/test/kotlin/no/nav/security/token/support/core/jwt/JwtTokenClaimsTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.core.jwt
2 |
3 | import com.nimbusds.jwt.JWTClaimsSet.Builder
4 | import com.nimbusds.jwt.PlainJWT
5 | import org.assertj.core.api.Assertions.assertThat
6 | import org.junit.jupiter.api.Test
7 |
8 | internal class JwtTokenClaimsTest {
9 |
10 | @Test
11 | fun containsClaimShouldHandleBothStringAndListClaim() {
12 | assertThat(withClaim("arrayClaim", listOf("1", "2")).containsClaim("arrayClaim", "1")).isTrue()
13 | assertThat(withClaim("stringClaim", "1").containsClaim("stringClaim", "1")).isTrue()
14 | }
15 |
16 | @Test
17 | fun containsClaimShouldHandleAsterisk() {
18 | assertThat(withClaim("stringClaim", "1").containsClaim("stringClaim", "*")).isTrue()
19 | assertThat(withClaim("emptyStringClaim", "").containsClaim("emptyStringClaim", "*")).isTrue()
20 | assertThat(withClaim("nullStringClaim", null).containsClaim("nullStringClaim", "*")).isFalse()
21 | assertThat(withClaim("arrayClaim", listOf("1", "2")).containsClaim("arrayClaim", "*")).isTrue()
22 | assertThat(withClaim("emptyArrayClaim", listOf()).containsClaim("emptyArrayClaim", "*")).isTrue()
23 | }
24 | private fun withClaim(name : String, value : Any?) = JwtTokenClaims(PlainJWT.parse(PlainJWT(Builder().claim(name, value).build()).serialize()).jwtClaimsSet)
25 | }
--------------------------------------------------------------------------------
/token-validation-core/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | %d{yyyy-MM-dd HH:mm:ss} %X{X-Nav-CallId} [%thread] %-5level %logger{70} - %msg%n
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/token-validation-core/src/test/resources/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "issuer": "$ISSUER",
3 | "authorization_endpoint": "na",
4 | "token_endpoint": "na",
5 | "end_session_endpoint": "na",
6 | "jwks_uri": "http://jwks",
7 | "response_modes_supported": [
8 | "query",
9 | "fragment",
10 | "form_post"
11 | ],
12 | "response_types_supported": [
13 | "code",
14 | "code id_token",
15 | "code token",
16 | "code id_token token",
17 | "id_token",
18 | "id_token token",
19 | "token",
20 | "token id_token"
21 | ],
22 | "scopes_supported": [
23 | "openid"
24 | ],
25 | "subject_types_supported": [
26 | "pairwise"
27 | ],
28 | "id_token_signing_alg_values_supported": [
29 | "RS256"
30 | ],
31 | "token_endpoint_auth_methods_supported": [
32 | "client_secret_post"
33 | ]
34 | }
--------------------------------------------------------------------------------
/token-validation-filter/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 |
12 | ### IntelliJ IDEA ###
13 | .idea
14 | *.iws
15 | *.iml
16 | *.ipr
17 |
18 | ### NetBeans ###
19 | nbproject/private/
20 | build/
21 | nbbuild/
22 | dist/
23 | nbdist/
24 | .nb-gradle/
25 |
--------------------------------------------------------------------------------
/token-validation-filter/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | no.nav.security
6 | token-support
7 | 3.0.0-SNAPSHOT
8 |
9 | token-validation-filter
10 | token-validation-filter
11 |
12 |
13 | ${project.groupId}
14 | token-validation-core
15 |
16 |
17 | jakarta.servlet
18 | jakarta.servlet-api
19 | provided
20 |
21 |
22 | org.slf4j
23 | slf4j-api
24 |
25 |
26 | ch.qos.logback
27 | logback-classic
28 | test
29 |
30 |
31 |
32 | ${project.basedir}/src/main/kotlin
33 | ${project.basedir}/src/test/kotlin
34 |
35 |
36 | kotlin-maven-plugin
37 | org.jetbrains.kotlin
38 |
39 |
40 | org.apache.maven.plugins
41 | maven-source-plugin
42 |
43 |
44 |
45 |
46 |
47 | release
48 |
49 |
50 |
51 | org.apache.maven.plugins
52 | maven-gpg-plugin
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/token-validation-filter/src/main/kotlin/no/nav/security/token/support/filter/JwtTokenValidationFilter.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.filter
2 |
3 | import jakarta.servlet.DispatcherType.*
4 | import jakarta.servlet.Filter
5 | import jakarta.servlet.FilterChain
6 | import jakarta.servlet.FilterConfig
7 | import jakarta.servlet.RequestDispatcher.ERROR_EXCEPTION
8 | import jakarta.servlet.ServletRequest
9 | import jakarta.servlet.ServletResponse
10 | import jakarta.servlet.http.HttpServletRequest
11 | import jakarta.servlet.http.HttpServletResponse
12 | import no.nav.security.token.support.core.context.TokenValidationContextHolder
13 | import no.nav.security.token.support.core.http.HttpRequest
14 | import no.nav.security.token.support.core.validation.JwtTokenValidationHandler
15 | import org.slf4j.LoggerFactory
16 |
17 | open class JwtTokenValidationFilter(private val jwtTokenValidationHandler : JwtTokenValidationHandler, private val contextHolder : TokenValidationContextHolder) : Filter {
18 | private val log = LoggerFactory.getLogger(JwtTokenValidationFilter::class.java)
19 | override fun destroy() {}
20 |
21 | override fun doFilter(request : ServletRequest, response : ServletResponse, chain : FilterChain) {
22 | if (request is HttpServletRequest) {
23 | if (request.dispatcherType == ASYNC &&(request.getAttribute(ERROR_EXCEPTION) as? Throwable)?.message?.contains("broken pipe", ignoreCase = true) == true) {
24 | log.trace("Skipping token validation for async request, client is gone")
25 | chain.doFilter(request, response)
26 | }
27 | else {
28 | doTokenValidation(request, response as HttpServletResponse, chain)
29 | }
30 | }
31 | else {
32 | chain.doFilter(request, response)
33 | }
34 | }
35 |
36 | override fun init(filterConfig : FilterConfig) {}
37 |
38 | private fun doTokenValidation(request : HttpServletRequest, response : HttpServletResponse, chain : FilterChain) {
39 | contextHolder.setTokenValidationContext(jwtTokenValidationHandler.getValidatedTokens(fromHttpServletRequest(request)))
40 | try {
41 | chain.doFilter(request, response)
42 | }
43 | finally {
44 | contextHolder.setTokenValidationContext(null)
45 | }
46 | }
47 |
48 | companion object {
49 |
50 | @JvmStatic
51 | fun fromHttpServletRequest(request: HttpServletRequest) = object : HttpRequest {
52 | override fun getHeader(headerName: String) = request.getHeader(headerName)
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/token-validation-filter/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | %d{yyyy-MM-dd HH:mm:ss} %X{X-Nav-CallId} [%thread] %-5level %logger{70} - %msg%n
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/token-validation-filter/src/test/resources/mockmetadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "issuer": "$ISSUER",
3 | "authorization_endpoint": "na",
4 | "token_endpoint": "na",
5 | "end_session_endpoint": "na",
6 | "jwks_uri": "http://jwks",
7 | "response_modes_supported": [
8 | "query",
9 | "fragment",
10 | "form_post"
11 | ],
12 | "response_types_supported": [
13 | "code",
14 | "code id_token",
15 | "code token",
16 | "code id_token token",
17 | "id_token",
18 | "id_token token",
19 | "token",
20 | "token id_token"
21 | ],
22 | "scopes_supported": [
23 | "openid"
24 | ],
25 | "subject_types_supported": [
26 | "pairwise"
27 | ],
28 | "id_token_signing_alg_values_supported": [
29 | "RS256"
30 | ],
31 | "token_endpoint_auth_methods_supported": [
32 | "client_secret_post"
33 | ]
34 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/main/kotlin/no/nav/security/token/support/jaxrs/JaxrsTokenValidationContextHolder.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs
2 |
3 | import no.nav.security.token.support.core.context.TokenValidationContext
4 | import no.nav.security.token.support.core.context.TokenValidationContextHolder
5 |
6 | object JaxrsTokenValidationContextHolder : TokenValidationContextHolder {
7 |
8 | private val validationContextHolder = ThreadLocal()
9 |
10 | fun getHolder() = JWT_BEARER_TOKEN_CONTEXT_HOLDER
11 | override fun getTokenValidationContext() = validationContextHolder.get()
12 |
13 | override fun setTokenValidationContext(tokenValidationContext: TokenValidationContext?) {
14 |
15 | if (validationContextHolder.get() != null && tokenValidationContext != null) {
16 | throw IllegalStateException("Should not overwrite the TokenValidationContext")
17 | }
18 | validationContextHolder.set(tokenValidationContext)
19 | }
20 |
21 | private val JWT_BEARER_TOKEN_CONTEXT_HOLDER: TokenValidationContextHolder
22 | get() = this
23 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/main/kotlin/no/nav/security/token/support/jaxrs/JwtTokenClientRequestFilter.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs
2 |
3 | import jakarta.inject.Inject
4 | import jakarta.ws.rs.client.ClientRequestContext
5 | import jakarta.ws.rs.client.ClientRequestFilter
6 | import no.nav.security.token.support.core.JwtTokenConstants.AUTHORIZATION_HEADER
7 | import no.nav.security.token.support.jaxrs.JaxrsTokenValidationContextHolder.getHolder
8 | import org.slf4j.Logger
9 | import org.slf4j.LoggerFactory
10 |
11 | class JwtTokenClientRequestFilter @Inject constructor() : ClientRequestFilter {
12 |
13 | override fun filter(requestContext : ClientRequestContext) {
14 | val context = getHolder().getTokenValidationContext()
15 |
16 | if (context.hasValidToken()) {
17 | LOG.debug("Adding tokens to Authorization header")
18 | val headerValue = context.issuers.joinToString(separator = "") {
19 | LOG.debug("Adding token for issuer $it")
20 | "Bearer ${context.getJwtToken(it)?.encodedToken}"
21 | }
22 | requestContext.headers[AUTHORIZATION_HEADER] = listOf(headerValue)
23 | } else {
24 | LOG.debug("No tokens found, nothing added to request")
25 | }
26 | }
27 |
28 | companion object {
29 |
30 | private val LOG : Logger = LoggerFactory.getLogger(JwtTokenClientRequestFilter::class.java)
31 | }
32 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/main/kotlin/no/nav/security/token/support/jaxrs/JwtTokenContainerRequestFilter.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs
2 |
3 | import jakarta.inject.Inject
4 | import jakarta.ws.rs.WebApplicationException
5 | import jakarta.ws.rs.container.ContainerRequestContext
6 | import jakarta.ws.rs.container.ContainerRequestFilter
7 | import jakarta.ws.rs.container.ResourceInfo
8 | import jakarta.ws.rs.core.Context
9 | import jakarta.ws.rs.core.Response.Status.FORBIDDEN
10 | import jakarta.ws.rs.core.Response.Status.UNAUTHORIZED
11 | import jakarta.ws.rs.ext.Provider
12 | import no.nav.security.token.support.core.exceptions.JwtTokenInvalidClaimException
13 | import no.nav.security.token.support.core.validation.JwtTokenAnnotationHandler
14 | import no.nav.security.token.support.jaxrs.JaxrsTokenValidationContextHolder.getHolder
15 |
16 | @Provider
17 | class JwtTokenContainerRequestFilter @Inject constructor() : ContainerRequestFilter {
18 |
19 | private val jwtTokenAnnotationHandler = JwtTokenAnnotationHandler(getHolder())
20 |
21 | @Context
22 | private lateinit var resourceInfo : ResourceInfo
23 |
24 | override fun filter(containerRequestContext : ContainerRequestContext) {
25 | val method = resourceInfo.resourceMethod
26 | try {
27 | jwtTokenAnnotationHandler.assertValidAnnotation(method)
28 | }
29 | catch (e : JwtTokenInvalidClaimException) {
30 | throw WebApplicationException(e, FORBIDDEN)
31 | }
32 | catch (e : Exception) {
33 | throw WebApplicationException(e, UNAUTHORIZED)
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/main/kotlin/no/nav/security/token/support/jaxrs/servlet/JaxrsJwtTokenValidationFilter.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs.servlet
2 |
3 | import no.nav.security.token.support.core.configuration.MultiIssuerConfiguration
4 | import no.nav.security.token.support.core.validation.JwtTokenValidationHandler
5 | import no.nav.security.token.support.filter.JwtTokenValidationFilter
6 | import no.nav.security.token.support.jaxrs.JaxrsTokenValidationContextHolder
7 |
8 | class JaxrsJwtTokenValidationFilter(oidcConfig : MultiIssuerConfiguration) : JwtTokenValidationFilter(JwtTokenValidationHandler(oidcConfig),
9 | JaxrsTokenValidationContextHolder.getHolder())
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/ClientFilterTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs
2 |
3 | import jakarta.ws.rs.client.ClientBuilder
4 | import java.util.concurrent.ConcurrentHashMap
5 | import no.nav.security.token.support.core.context.TokenValidationContext
6 | import no.nav.security.token.support.core.jwt.JwtToken
7 | import no.nav.security.token.support.jaxrs.JaxrsTokenValidationContextHolder.getHolder
8 | import org.hamcrest.MatcherAssert
9 | import org.hamcrest.Matchers
10 | import org.hamcrest.core.Is
11 | import org.junit.jupiter.api.Test
12 | import org.springframework.boot.test.context.SpringBootTest
13 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
14 | import org.springframework.boot.test.web.server.LocalServerPort
15 | import org.springframework.test.annotation.DirtiesContext
16 | import org.springframework.test.context.ActiveProfiles
17 |
18 | @ActiveProfiles("protected")
19 | @DirtiesContext
20 | @SpringBootTest(webEnvironment = RANDOM_PORT, classes = [Config::class])
21 | internal class ClientFilterTest {
22 |
23 | @LocalServerPort
24 | private val port = 0
25 |
26 | private fun request() = ClientBuilder.newClient()
27 | .register(JwtTokenClientRequestFilter::class.java)
28 | .target("http://localhost:$port")
29 | .path("echo/token")
30 | .request()
31 |
32 | @Test
33 | fun that_unprotected_returns_ok_with_valid_token() {
34 | val token = JwtTokenGenerator.createSignedJWT("12345678911").serialize()
35 | addTokenToContextHolder(token)
36 | val returnedToken = request().get().readEntity(String::class.java)
37 | MatcherAssert.assertThat(returnedToken, Is.`is`(Matchers.equalTo(token)))
38 | }
39 |
40 | /**
41 | * Adds the token to the context holder, so it is available for the
42 | * [JwtTokenClientRequestFilter]. This is basically what the
43 | * [JwtTokenValidationFilter] filter does
44 | */
45 | private fun addTokenToContextHolder(token : String) {
46 | getHolder().setTokenValidationContext(createOidcValidationContext("protected", JwtToken(token)))
47 | }
48 |
49 | companion object {
50 |
51 | private fun createOidcValidationContext(issuerShortName : String, jwtToken : JwtToken) =
52 | TokenValidationContext(ConcurrentHashMap().apply {
53 | put(issuerShortName, jwtToken)
54 | })
55 | }
56 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/FileResourceRetriever.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs
2 |
3 | import com.nimbusds.common.contenttype.ContentType.APPLICATION_JSON
4 | import com.nimbusds.jose.util.IOUtils
5 | import com.nimbusds.jose.util.Resource
6 | import java.io.IOException
7 | import java.net.URL
8 | import java.nio.charset.StandardCharsets
9 | import no.nav.security.token.support.core.configuration.ProxyAwareResourceRetriever
10 |
11 | internal class FileResourceRetriever(private val metadataFile : String, private val jwksFile : String) : ProxyAwareResourceRetriever() {
12 |
13 | override fun retrieveResource(url : URL) =
14 | getContentFromFile(url)?.let { Resource(it, APPLICATION_JSON.type) } ?: super.retrieveResource(url)
15 |
16 | private fun getContentFromFile(url : URL) : String? {
17 | try {
18 | if (url.toString().contains("metadata")) {
19 | return IOUtils.readInputStreamToString(getInputStream(metadataFile), StandardCharsets.UTF_8)
20 | }
21 | if (url.toString().contains("jwks")) {
22 | return IOUtils.readInputStreamToString(getInputStream(jwksFile), StandardCharsets.UTF_8)
23 | }
24 | return null
25 | }
26 | catch (e : IOException) {
27 | throw RuntimeException(e)
28 | }
29 | }
30 |
31 | private fun getInputStream(file : String) = FileResourceRetriever::class.java.getResourceAsStream(file)
32 |
33 | override fun toString() = javaClass.simpleName + " [metadataFile=" + metadataFile + ", jwksFile=" + jwksFile + "]"
34 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/JwkGenerator.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs
2 |
3 | import com.nimbusds.jose.jwk.JWKSet.parse
4 | import com.nimbusds.jose.jwk.RSAKey
5 | import com.nimbusds.jose.util.IOUtils.readInputStreamToString
6 | import java.nio.charset.StandardCharsets.UTF_8
7 |
8 | internal object JwkGenerator {
9 |
10 | private const val DEFAULT_KEYID = "localhost-signer"
11 | const val DEFAULT_JWKSET_FILE : String = "/jwkset.json"
12 |
13 | val jWKSet = parse(readInputStreamToString(JwkGenerator::class.java.getResourceAsStream(DEFAULT_JWKSET_FILE), UTF_8))
14 |
15 | val defaultRSAKey = jWKSet.getKeyByKeyId(DEFAULT_KEYID) as RSAKey
16 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/JwtTokenGenerator.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs
2 |
3 | import com.nimbusds.jose.JOSEObjectType.JWT
4 | import com.nimbusds.jose.JWSAlgorithm.RS256
5 | import com.nimbusds.jose.JWSHeader
6 | import com.nimbusds.jose.crypto.RSASSASigner
7 | import com.nimbusds.jose.jwk.RSAKey
8 | import com.nimbusds.jwt.JWTClaimsSet
9 | import com.nimbusds.jwt.JWTClaimsSet.Builder
10 | import com.nimbusds.jwt.SignedJWT
11 | import java.util.*
12 | import java.util.concurrent.TimeUnit.MINUTES
13 | import no.nav.security.token.support.jaxrs.JwkGenerator.defaultRSAKey
14 |
15 | internal object JwtTokenGenerator {
16 |
17 | const val ISS : String = "iss-localhost"
18 | const val AUD : String = "aud-localhost"
19 | const val ACR : String = "Level4"
20 | const val EXPIRY : Long = (60 * 60 * 3600).toLong()
21 |
22 | fun createSignedJWT(subject : String?, expiryInMinutes : Long = EXPIRY) =
23 | createSignedJWT(defaultRSAKey, buildClaimSet(subject, ISS, AUD, ACR, MINUTES.toMillis(expiryInMinutes)))
24 |
25 | fun buildClaimSet(subject : String?, issuer : String?, audience : String?, authLevel : String?, expiry : Long) : JWTClaimsSet =
26 | Date().run {
27 | Builder()
28 | .subject(subject)
29 | .issuer(issuer)
30 | .audience(audience)
31 | .jwtID(UUID.randomUUID().toString())
32 | .claim("acr", authLevel)
33 | .claim("ver", "1.0")
34 | .claim("nonce", "myNonce")
35 | .claim("auth_time", this)
36 | .notBeforeTime(this)
37 | .issueTime(this)
38 | .expirationTime(Date(time + expiry)).build()
39 | }
40 |
41 | fun createSignedJWT(rsaJwk : RSAKey, claimsSet : JWTClaimsSet?) =
42 | SignedJWT(JWSHeader.Builder(RS256)
43 | .keyID(rsaJwk.keyID)
44 | .type(JWT).build(), claimsSet).apply {
45 | sign(RSASSASigner(rsaJwk.toPrivateKey()))
46 | }
47 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/ServerFilterProtectedClassUnknownIssuerTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs
2 |
3 | import jakarta.ws.rs.client.ClientBuilder
4 | import jakarta.ws.rs.client.Invocation.Builder
5 | import no.nav.security.token.support.core.JwtTokenConstants.AUTHORIZATION_HEADER
6 | import no.nav.security.token.support.core.jwt.JwtToken.Companion.asBearer
7 | import no.nav.security.token.support.jaxrs.JwtTokenGenerator.createSignedJWT
8 | import org.hamcrest.MatcherAssert
9 | import org.hamcrest.Matchers
10 | import org.hamcrest.core.Is
11 | import org.junit.jupiter.api.Test
12 | import org.springframework.boot.test.context.SpringBootTest
13 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
14 | import org.springframework.boot.test.web.server.LocalServerPort
15 | import org.springframework.test.annotation.DirtiesContext
16 | import org.springframework.test.context.ActiveProfiles
17 |
18 | @ActiveProfiles("invalid")
19 | @DirtiesContext
20 | @SpringBootTest(webEnvironment = RANDOM_PORT, classes = [Config::class])
21 | internal class ServerFilterProtectedClassUnknownIssuerTest {
22 |
23 | @LocalServerPort
24 | private val port = 0
25 |
26 | private fun requestWithInvalidClaimsToken(path : String) : Builder {
27 | return ClientBuilder.newClient().target("http://localhost:$port")
28 | .path(path)
29 | .request()
30 | .header(AUTHORIZATION_HEADER, createSignedJWT("12345678911").asBearer())
31 | }
32 |
33 | @Test
34 | fun that_unprotected_returns_ok_with_invalid_token() {
35 | val response = requestWithInvalidClaimsToken("class/unprotected").get()
36 | MatcherAssert.assertThat(response.status, Is.`is`(Matchers.equalTo(200)))
37 | }
38 |
39 | @Test
40 | fun that_protected_returns_200_with_any_token() {
41 | val response = requestWithInvalidClaimsToken("class/protected").get()
42 | MatcherAssert.assertThat(response.status, Is.`is`(Matchers.equalTo(200)))
43 | }
44 |
45 | @Test
46 | fun that_protected_with_claims_returns_401_with_invalid_token() {
47 | val response = requestWithInvalidClaimsToken("class/protected/with/claims").get()
48 | MatcherAssert.assertThat(response.status, Is.`is`(Matchers.equalTo(401)))
49 | }
50 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/ServerFilterProtectedMethodUnknownIssuerTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs
2 |
3 | import jakarta.ws.rs.client.ClientBuilder
4 | import no.nav.security.token.support.core.JwtTokenConstants.AUTHORIZATION_HEADER
5 | import no.nav.security.token.support.jaxrs.JwtTokenGenerator.createSignedJWT
6 | import org.junit.jupiter.api.Assertions.assertEquals
7 | import org.junit.jupiter.api.Test
8 | import org.springframework.boot.test.context.SpringBootTest
9 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
10 | import org.springframework.boot.test.web.server.LocalServerPort
11 | import org.springframework.http.HttpStatus.OK
12 | import org.springframework.http.HttpStatus.UNAUTHORIZED
13 | import org.springframework.test.annotation.DirtiesContext
14 | import org.springframework.test.context.ActiveProfiles
15 |
16 | @ActiveProfiles("invalid")
17 | @DirtiesContext
18 | @SpringBootTest(webEnvironment = RANDOM_PORT, classes = [Config::class])
19 | internal class ServerFilterProtectedMethodUnknownIssuerTest {
20 |
21 | @LocalServerPort
22 | private val port = 0
23 |
24 | private fun requestWithInvalidClaimsToken(path : String) =
25 | ClientBuilder.newClient().target("http://localhost:$port")
26 | .path(path)
27 | .request()
28 | .header(AUTHORIZATION_HEADER, "Bearer " + createSignedJWT("12345678911").serialize())
29 | .get()
30 |
31 | @Test
32 | fun that_unprotected_returns_ok_with_invalid_token() {
33 | assertEquals(OK.value(), requestWithInvalidClaimsToken("unprotected").status)
34 | }
35 |
36 | @Test
37 | fun that_protected_returns_200_with_any_token() {
38 | assertEquals(OK.value(), requestWithInvalidClaimsToken("protected").status)
39 | }
40 |
41 | @Test
42 | fun that_protected_with_claims_returns_401_with_invalid_token() {
43 | assertEquals(UNAUTHORIZED.value(), requestWithInvalidClaimsToken("protected/with/claims").status)
44 | }
45 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/TestTokenGeneratorResource.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs
2 |
3 | import com.nimbusds.jose.util.IOUtils.readInputStreamToString
4 | import jakarta.servlet.http.HttpServletRequest
5 | import jakarta.ws.rs.DefaultValue
6 | import jakarta.ws.rs.GET
7 | import jakarta.ws.rs.Path
8 | import jakarta.ws.rs.QueryParam
9 | import jakarta.ws.rs.core.Context
10 | import java.nio.charset.Charset.defaultCharset
11 | import java.util.Objects.requireNonNull
12 | import no.nav.security.token.support.core.api.Unprotected
13 | import no.nav.security.token.support.jaxrs.JwkGenerator.DEFAULT_JWKSET_FILE
14 | import no.nav.security.token.support.jaxrs.JwkGenerator.jWKSet
15 | import no.nav.security.token.support.jaxrs.JwtTokenGenerator.createSignedJWT
16 |
17 | @Path("local")
18 | class TestTokenGeneratorResource {
19 |
20 | @Unprotected
21 | @GET
22 | fun endpoints(@Context request: HttpServletRequest) = arrayOf(
23 | TokenEndpoint("Get JWT as serialized string", "${request.requestURL}/jwt", "subject"),
24 | TokenEndpoint("Get JWT as SignedJWT object with claims", "${request.requestURL}/claims", "subject"),
25 | TokenEndpoint("Get JWKS used to sign token", "${request.requestURL}/jwks"),
26 | TokenEndpoint("Get JWKS used to sign token as JWKSet object", "${request.requestURL}/jwkset"),
27 | TokenEndpoint("Get token issuer metadata (ref oidc .well-known)", "${request.requestURL}/metadata"))
28 | @Unprotected
29 | @Path("/jwt")
30 | @GET
31 | fun issueToken(@QueryParam("subject") @DefaultValue("12345678910") subject : String?) = createSignedJWT(subject).serialize()
32 |
33 | @Unprotected
34 | @Path("/claims")
35 | @GET
36 | fun jwtClaims(@QueryParam("subject") @DefaultValue("12345678910") subject : String?) = createSignedJWT(subject)
37 |
38 | @Unprotected
39 | @GET
40 | @Path("/jwks")
41 | fun jwks() = readInputStreamToString(requireNonNull(javaClass.getResourceAsStream(DEFAULT_JWKSET_FILE)), defaultCharset())
42 |
43 | @Unprotected
44 | @GET
45 | @Path("jwkset")
46 | fun jwkSet() = jWKSet
47 |
48 | @Unprotected
49 | @GET
50 | @Path("/metadata")
51 | fun metadata() = readInputStreamToString(requireNonNull(javaClass.getResourceAsStream("/metadata.json")), defaultCharset())
52 |
53 | class TokenEndpoint(val desc : String, val uri : String, vararg val params : String)
54 |
55 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/rest/ProtectedClassResource.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs.rest
2 |
3 | import jakarta.ws.rs.GET
4 | import jakarta.ws.rs.Path
5 | import jakarta.ws.rs.core.Response.ok
6 | import no.nav.security.token.support.core.api.Protected
7 |
8 | @Path("class/protected")
9 | @Protected
10 | class ProtectedClassResource {
11 |
12 | @GET
13 | fun get() = ok().build()
14 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/rest/ProtectedMethodResource.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs.rest
2 |
3 | import jakarta.ws.rs.GET
4 | import jakarta.ws.rs.Path
5 | import jakarta.ws.rs.core.Response.ok
6 | import no.nav.security.token.support.core.api.Protected
7 | import no.nav.security.token.support.core.api.ProtectedWithClaims
8 | import no.nav.security.token.support.core.api.Unprotected
9 |
10 | @Path("")
11 | class ProtectedMethodResource {
12 | @GET
13 | @Path("unprotected")
14 | @Unprotected
15 | fun unprotected() = ok().build()
16 | @GET
17 | @Path("protected")
18 | @Protected
19 | fun protectedMethod()= ok().build()
20 | @GET
21 | @Path("protected/with/claims")
22 | @ProtectedWithClaims(issuer = "protected", claimMap = ["acr=Level4"])
23 | fun protectedWithClaims() = ok().build()
24 |
25 | @GET
26 | @Path("protected/with/claims/unknown")
27 | @ProtectedWithClaims(issuer = "protected", claimMap = ["acr=Level5"])
28 | fun protectedWithUnknownClaims() = ok().build()
29 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/rest/ProtectedWithClaimsClassResource.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs.rest
2 |
3 | import jakarta.ws.rs.GET
4 | import jakarta.ws.rs.Path
5 | import jakarta.ws.rs.core.Response.ok
6 | import no.nav.security.token.support.core.api.ProtectedWithClaims
7 |
8 | @Path("class/protected/with/claims")
9 | @ProtectedWithClaims(issuer = "protected", claimMap = ["acr=Level4"])
10 | class ProtectedWithClaimsClassResource {
11 |
12 | @GET
13 | fun get() = ok().build()
14 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/rest/TokenResource.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs.rest
2 |
3 | import jakarta.ws.rs.GET
4 | import jakarta.ws.rs.Path
5 | import jakarta.ws.rs.core.Response.ok
6 | import no.nav.security.token.support.core.api.Unprotected
7 | import no.nav.security.token.support.jaxrs.JaxrsTokenValidationContextHolder.getHolder
8 |
9 | @Path("echo")
10 | @Unprotected
11 | class TokenResource {
12 |
13 | @get:Path("token")
14 | @get:GET
15 | val token = ok()
16 | .entity(getHolder().getTokenValidationContext().getJwtToken("protected")!!.encodedToken)
17 | .build()
18 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/rest/UnprotectedClassResource.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs.rest
2 |
3 | import jakarta.ws.rs.GET
4 | import jakarta.ws.rs.Path
5 | import jakarta.ws.rs.core.Response.ok
6 | import no.nav.security.token.support.core.api.Unprotected
7 |
8 | @Path("class/unprotected")
9 | @Unprotected
10 | class UnprotectedClassResource {
11 | @GET
12 | fun get() = ok().build()
13 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/kotlin/no/nav/security/token/support/jaxrs/rest/WithoutAnnotationsResource.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.jaxrs.rest
2 |
3 | import jakarta.ws.rs.GET
4 | import jakarta.ws.rs.Path
5 | import jakarta.ws.rs.core.Response.ok
6 |
7 | @Path("without/annotations")
8 | class WithoutAnnotationsResource {
9 |
10 | @GET
11 | fun get() = ok().build()
12 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/resources/application-invalid.yaml:
--------------------------------------------------------------------------------
1 | spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
2 | no.nav.security.jwt:
3 | issuers: invalid
4 | issuer.invalid:
5 | discovery-url: http://metadata
6 | accepted-audience: aud-localhost
7 | debug: false
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/resources/application-protected.yaml:
--------------------------------------------------------------------------------
1 | spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
2 | no.nav.security.jwt:
3 | issuers: protected
4 | issuer.protected:
5 | discovery-url: http://metadata
6 | accepted_audience: aud-localhost
7 | debug: false
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/resources/jwkset.json:
--------------------------------------------------------------------------------
1 | {
2 | "keys": [
3 | {
4 | "kty": "RSA",
5 | "d": "MRf73iiXUEhJFxDTtJ5rEHNQsAG8XFuXkz9vXXbMp1_OTo11bEx3SnHiwmO_mSAAeXWNJniLw07V1-nk551h5in_ueAPwXTOf8qddacvDEBZwcxeqfu_Kjh1R0ji8Xn1a037CpH2IO34Lyw2gmsGFdMZgDwa5Z0KJjPCU6W8tF6CA-2omAdNzrFaWtaPFpBC0NzYaaB111bKIXxngG97Cnu81deEEKmX-vL-O4tpvUUybuquxrlFvVlTeYlrQqv50_IKsKSYkg-iu1cbqIiWrRq9eTmA6EppmZbqHjKSM5JYFbPB_oZ9QeHKnp1_MTom-jKMEpw18qq-PzdX_skZWQ",
6 | "e": "AQAB",
7 | "use": "sig",
8 | "kid": "localhost-signer",
9 | "alg": "RS256",
10 | "n": "lFTMP9TSUwLua0G8M7foqmdUS2us1-JOF8H_tClVG3IEQMRvMmHJoGSdldWDHsNwRG3Wevl_8fZoGocw9hPqj93j-vI4-ZkbxwhPyRqlS0FNIPD1Ln5R6AmHu7b-paRIz3lvqpyTRwnGBI9weE4u6WOpOQ8DjJMNPq4WcM42AgDJAvc6UuhcWW_MLIsjkKp_VYKxzthSuiRAxXi8Pz4ZhiTAEZI-UN61DYU9YEFNujg5XtIQsRwQn1Vj7BknGwkdf_iCGJgDlKUOz9hAojOMXTAwetUx6I5nngIM5vaXWJCmKn6SzcTYgHWWVrn8qaSazioaydLaYN9NuQ0MdIvsQw"
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/resources/jwtkeystore.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/navikt/token-support/731015c17f10aa7d9286dea5b0480c1837d7944e/token-validation-jaxrs/src/test/resources/jwtkeystore.jks
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/token-validation-jaxrs/src/test/resources/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "issuer": "iss-localhost",
3 | "authorization_endpoint": "na",
4 | "token_endpoint": "na",
5 | "end_session_endpoint": "na",
6 | "jwks_uri": "http://jwks",
7 | "response_modes_supported": [
8 | "query",
9 | "fragment",
10 | "form_post"
11 | ],
12 | "response_types_supported": [
13 | "code",
14 | "code id_token",
15 | "code token",
16 | "code id_token token",
17 | "id_token",
18 | "id_token token",
19 | "token",
20 | "token id_token"
21 | ],
22 | "scopes_supported": [
23 | "openid"
24 | ],
25 | "subject_types_supported": [
26 | "pairwise"
27 | ],
28 | "id_token_signing_alg_values_supported": [
29 | "RS256"
30 | ],
31 | "token_endpoint_auth_methods_supported": [
32 | "client_secret_post"
33 | ]
34 | }
--------------------------------------------------------------------------------
/token-validation-ktor-demo/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 |
12 | ### IntelliJ IDEA ###
13 | .idea
14 | *.iws
15 | *.iml
16 | *.ipr
17 |
18 | ### NetBeans ###
19 | nbproject/private/
20 | build/
21 | nbbuild/
22 | dist/
23 | nbdist/
24 | .nb-gradle/
25 |
--------------------------------------------------------------------------------
/token-validation-ktor-demo/resources/application.conf:
--------------------------------------------------------------------------------
1 | ktor {
2 | deployment {
3 | port = 8085
4 | port = ${?PORT}
5 | }
6 | application {
7 | modules = [ com.example.ApplicationKt.module ]
8 | }
9 | }
10 |
11 | no.nav.security.jwt {
12 | issuers = [
13 | {
14 | issuer_name = someshortname
15 | discoveryurl = "http://metadata"
16 | discoveryurl = ${?OIDC_DISCOVERY_URL}
17 | accepted_audience = aud-localhost
18 | accepted_audience = ${?OIDC_ACCEPTED_AUDIENCE}
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/token-validation-ktor-demo/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/token-validation-ktor-v2/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 |
12 | ### IntelliJ IDEA ###
13 | .idea
14 | *.iws
15 | *.iml
16 | *.ipr
17 |
18 | ### NetBeans ###
19 | nbproject/private/
20 | build/
21 | nbbuild/
22 | dist/
23 | nbdist/
24 | .nb-gradle/
25 |
--------------------------------------------------------------------------------
/token-validation-ktor-v2/src/main/kotlin/no/nav/security/token/support/v2/JwtTokenExpiryThresholdHandler.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.v2
2 |
3 | import com.nimbusds.jwt.JWTClaimNames.EXPIRATION_TIME
4 | import io.ktor.server.application.*
5 | import io.ktor.server.response.*
6 | import java.time.LocalDateTime.now
7 | import java.time.LocalDateTime.ofInstant
8 | import java.time.ZoneId.systemDefault
9 | import java.time.temporal.ChronoUnit.MINUTES
10 | import java.util.*
11 | import no.nav.security.token.support.core.JwtTokenConstants.TOKEN_EXPIRES_SOON_HEADER
12 | import no.nav.security.token.support.core.context.TokenValidationContext
13 | import no.nav.security.token.support.core.jwt.JwtTokenClaims
14 | import org.slf4j.LoggerFactory
15 |
16 | class JwtTokenExpiryThresholdHandler(private val expiryThreshold: Int) {
17 |
18 | private val log = LoggerFactory.getLogger(JwtTokenExpiryThresholdHandler::class.java.name)
19 |
20 | fun addHeaderOnTokenExpiryThreshold(call: ApplicationCall, ctx: TokenValidationContext) {
21 | if(expiryThreshold > 0) {
22 | ctx.issuers.forEach {
23 | if (tokenExpiresBeforeThreshold(ctx.getClaims(it))) {
24 | call.response.header(TOKEN_EXPIRES_SOON_HEADER, "true")
25 | } else {
26 | log.debug("Token is still within expiry threshold.")
27 | }
28 | }
29 | } else {
30 | log.debug("Expiry threshold is not set")
31 | }
32 | }
33 |
34 | private fun tokenExpiresBeforeThreshold(jwtTokenClaims: JwtTokenClaims): Boolean {
35 | val expiryDate = jwtTokenClaims.get(EXPIRATION_TIME) as Date
36 | val expiry = ofInstant(expiryDate.toInstant(), systemDefault())
37 | val minutesUntilExpiry = now().until(expiry, MINUTES)
38 | log.debug("Checking token at time {} with expirationTime {} for how many minutes until expiry: {}",
39 | now(), expiry, minutesUntilExpiry)
40 | if (minutesUntilExpiry <= expiryThreshold) {
41 | log.debug("There are {} minutes until expiry which is equal to or less than the configured threshold {}",
42 | minutesUntilExpiry, expiryThreshold)
43 | return true
44 | }
45 | return false
46 | }
47 | }
--------------------------------------------------------------------------------
/token-validation-ktor-v2/src/test/kotlin/no/nav/security/token/support/v2/JwkGenerator.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.v2
2 |
3 | import com.nimbusds.jose.jwk.JWKSet
4 | import com.nimbusds.jose.jwk.RSAKey
5 | import com.nimbusds.jose.util.IOUtils
6 | import java.io.IOException
7 | import java.nio.charset.StandardCharsets
8 | import java.text.ParseException
9 |
10 |
11 | object JwkGenerator {
12 | const val DEFAULT_KEYID = "localhost-signer"
13 | const val DEFAULT_JWKSET_FILE = "/jwkset.json"
14 | val defaultRSAKey: RSAKey
15 | get() = jWKSet.getKeyByKeyId(DEFAULT_KEYID) as RSAKey
16 |
17 | val jWKSet: JWKSet
18 | get() = try {
19 | JWKSet.parse(
20 | IOUtils.readInputStreamToString(
21 | JwkGenerator::class.java.getResourceAsStream(DEFAULT_JWKSET_FILE), StandardCharsets.UTF_8))
22 | } catch (io: IOException) {
23 | throw RuntimeException(io)
24 | } catch (io: ParseException) {
25 | throw RuntimeException(io)
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/token-validation-ktor-v2/src/test/kotlin/no/nav/security/token/support/v2/JwtTokenGenerator.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.v2
2 |
3 | import com.nimbusds.jose.JOSEException
4 | import com.nimbusds.jose.JOSEObjectType
5 | import com.nimbusds.jose.JWSAlgorithm
6 | import com.nimbusds.jose.JWSHeader
7 | import com.nimbusds.jose.JWSSigner
8 | import com.nimbusds.jose.crypto.RSASSASigner
9 | import com.nimbusds.jose.jwk.RSAKey
10 | import com.nimbusds.jwt.JWTClaimsSet
11 | import com.nimbusds.jwt.JWTClaimsSet.Builder
12 | import com.nimbusds.jwt.SignedJWT
13 | import java.util.*
14 | import java.util.concurrent.TimeUnit.MINUTES
15 |
16 | object JwtTokenGenerator {
17 | const val ISS = "iss-localhost"
18 | const val AUD = "aud-localhost"
19 | const val ACR = "Level4"
20 | const val EXPIRY = (60 * 60 * 3600).toLong()
21 | fun signedJWTAsString(subject: String?): String {
22 | return createSignedJWT(subject).serialize()
23 | }
24 |
25 | @JvmOverloads
26 | fun createSignedJWT(subject: String?, expiryInMinutes: Long = EXPIRY): SignedJWT {
27 | val claimsSet = buildClaimSet(subject, ISS, AUD, ACR, MINUTES.toMillis(expiryInMinutes))
28 | return createSignedJWT(
29 | JwkGenerator.defaultRSAKey,
30 | claimsSet)
31 | }
32 |
33 | fun createSignedJWT(claimsSet: JWTClaimsSet?): SignedJWT {
34 | return createSignedJWT(JwkGenerator.defaultRSAKey, claimsSet)
35 | }
36 |
37 | fun buildClaimSet(subject: String?, issuer: String?, audience: String?, authLevel: String?,
38 | expiry: Long): JWTClaimsSet {
39 | val now = Date()
40 | return Builder()
41 | .subject(subject)
42 | .issuer(issuer)
43 | .audience(audience)
44 | .jwtID(UUID.randomUUID().toString())
45 | .claim("acr", authLevel)
46 | .claim("ver", "1.0")
47 | .claim("nonce", "myNonce")
48 | .claim("auth_time", now)
49 | .notBeforeTime(now)
50 | .issueTime(now)
51 | .expirationTime(Date(now.time + expiry)).build()
52 | }
53 |
54 | fun createSignedJWT(rsaJwk: RSAKey, claimsSet: JWTClaimsSet?): SignedJWT {
55 | return try {
56 | val header = JWSHeader.Builder(JWSAlgorithm.RS256)
57 | .keyID(rsaJwk.keyID)
58 | .type(JOSEObjectType.JWT)
59 | val signedJWT = SignedJWT(header.build(), claimsSet)
60 | val signer: JWSSigner = RSASSASigner(rsaJwk.toPrivateKey())
61 | signedJWT.sign(signer)
62 | signedJWT
63 | } catch (e: JOSEException) {
64 | throw RuntimeException(e)
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/token-validation-ktor-v2/src/test/kotlin/no/nav/security/token/support/v2/TokenSupportAuthenticationProviderKtTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.v2
2 |
3 | import com.nimbusds.jwt.JWTClaimNames.SUBJECT
4 | import io.kotest.assertions.asClue
5 | import io.kotest.matchers.shouldBe
6 | import io.ktor.server.config.*
7 | import no.nav.security.mock.oauth2.withMockOAuth2Server
8 | import no.nav.security.token.support.core.configuration.IssuerProperties
9 | import org.junit.jupiter.api.Test
10 |
11 | internal class TokenSupportAuthenticationProviderKtTest {
12 |
13 | @Test
14 | fun `config properties are parsed correctly`() {
15 | withMockOAuth2Server {
16 | val config = MapApplicationConfig(
17 | "no.nav.security.jwt.expirythreshold" to "5",
18 | "no.nav.security.jwt.issuers.size" to "1",
19 | "no.nav.security.jwt.issuers.0.issuer_name" to "da issuah",
20 | "no.nav.security.jwt.issuers.0.discoveryurl" to this.wellKnownUrl("whatever").toString(),
21 | "no.nav.security.jwt.issuers.0.accepted_audience" to "da audienze",
22 | "no.nav.security.jwt.issuers.0.jwks_cache.lifespan" to "20",
23 | "no.nav.security.jwt.issuers.0.jwks_cache.refreshtime" to "57",
24 | "no.nav.security.jwt.issuers.0.validation.optional_claims" to SUBJECT
25 | )
26 |
27 | config.asIssuerProps().asClue {
28 | it["da issuah"]?.acceptedAudience shouldBe listOf("da audienze")
29 | it["da issuah"]?.discoveryUrl shouldBe this.wellKnownUrl("whatever").toUrl()
30 | it["da issuah"]?.jwksCache shouldBe IssuerProperties.JwksCache(20, 57)
31 | it["da issuah"]?.validation shouldBe IssuerProperties.Validation(listOf(SUBJECT))
32 | }
33 | }
34 | }
35 |
36 |
37 | }
--------------------------------------------------------------------------------
/token-validation-ktor-v2/src/test/kotlin/no/nav/security/token/support/v2/inlineconfigtestapp/InlineConfigApplication.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.v2.inlineconfigtestapp
2 |
3 | import com.nimbusds.jose.util.DefaultResourceRetriever
4 | import io.ktor.http.*
5 | import io.ktor.server.application.*
6 | import io.ktor.server.auth.*
7 | import io.ktor.server.netty.*
8 | import io.ktor.server.response.*
9 | import io.ktor.server.routing.*
10 | import no.nav.security.token.support.core.configuration.ProxyAwareResourceRetriever.Companion.DEFAULT_HTTP_CONNECT_TIMEOUT
11 | import no.nav.security.token.support.core.configuration.ProxyAwareResourceRetriever.Companion.DEFAULT_HTTP_READ_TIMEOUT
12 | import no.nav.security.token.support.core.configuration.ProxyAwareResourceRetriever.Companion.DEFAULT_HTTP_SIZE_LIMIT
13 | import no.nav.security.token.support.v2.IssuerConfig
14 | import no.nav.security.token.support.v2.TokenSupportConfig
15 | import no.nav.security.token.support.v2.tokenValidationSupport
16 |
17 | fun main(args: Array): Unit = EngineMain.main(args)
18 |
19 | var helloCounter = 0
20 |
21 | fun Application.inlineConfiguredModule() {
22 | install(Authentication) {
23 | tokenValidationSupport(config = TokenSupportConfig(IssuerConfig("iss-localhost", "http://localhost:33445/.well-known/openid-configuration", listOf("aud-localhost", "anotherAudience"))), resourceRetriever = DefaultResourceRetriever(DEFAULT_HTTP_CONNECT_TIMEOUT, DEFAULT_HTTP_READ_TIMEOUT, DEFAULT_HTTP_SIZE_LIMIT))
24 | }
25 | routing {
26 | authenticate {
27 | get("/inlineconfig") {
28 | helloCounter++
29 | call.respondText("Authenticated hello with inline config", ContentType.Text.Html)
30 | }
31 | }
32 | }
33 |
34 |
35 | }
--------------------------------------------------------------------------------
/token-validation-ktor-v2/src/test/kotlin/no/nav/security/token/support/v2/testapp/TestApplication.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.v2.testapp
2 |
3 | import io.ktor.http.*
4 | import io.ktor.server.application.*
5 | import io.ktor.server.auth.*
6 | import io.ktor.server.response.*
7 | import io.ktor.server.routing.*
8 | import no.nav.security.token.support.v2.RequiredClaims
9 | import no.nav.security.token.support.v2.TokenValidationContextPrincipal
10 | import no.nav.security.token.support.v2.tokenValidationSupport
11 |
12 | fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args)
13 |
14 | @Suppress("unused") // Referenced in application.conf
15 | fun Application.module() {
16 |
17 | val config = this.environment.config
18 | val acceptedIssuer = "default"
19 |
20 | install(Authentication) {
21 | tokenValidationSupport("validToken", config = config)
22 | tokenValidationSupport("validUser", config = config,
23 | requiredClaims = RequiredClaims(issuer = acceptedIssuer, claimMap = arrayOf("NAVident=X112233"))
24 | )
25 | tokenValidationSupport("validGroup", config = config,
26 | additionalValidation = {
27 | val claims = it.getClaims(acceptedIssuer)
28 | val groups = claims.getAsList("groups")
29 | val hasGroup = groups != null && groups.contains("THEGROUP")
30 | val hasIdentRequiredForAuditLog = claims.getStringClaim("NAVident") != null
31 | hasGroup && hasIdentRequiredForAuditLog
32 | })
33 | }
34 |
35 | routing {
36 | authenticate("validToken") {
37 | get("/hello") {
38 |
39 | call.respondText("Authenticated hello", ContentType.Text.Html)
40 | }
41 | }
42 |
43 | authenticate("validUser") {
44 | get("/hello_person") {
45 | call.respondText("Hello X112233", ContentType.Text.Html)
46 | }
47 | }
48 |
49 | authenticate("validGroup") {
50 | get("/hello_group") {
51 | val principal: TokenValidationContextPrincipal? = call.authentication.principal()
52 | val ident = principal?.context?.getClaims(acceptedIssuer)?.getStringClaim("NAVident")
53 | println("NAVident = $ident is accessing hello_group")
54 | call.respondText("Hello THEGROUP", ContentType.Text.Html)
55 | }
56 | }
57 |
58 | get("/openhello") {
59 | call.respondText("Hello in the open", ContentType.Text.Html)
60 | }
61 |
62 | }
63 |
64 |
65 | }
--------------------------------------------------------------------------------
/token-validation-ktor-v2/src/test/resources/jwkset.json:
--------------------------------------------------------------------------------
1 | {
2 | "keys": [
3 | {
4 | "kty": "RSA",
5 | "d": "MRf73iiXUEhJFxDTtJ5rEHNQsAG8XFuXkz9vXXbMp1_OTo11bEx3SnHiwmO_mSAAeXWNJniLw07V1-nk551h5in_ueAPwXTOf8qddacvDEBZwcxeqfu_Kjh1R0ji8Xn1a037CpH2IO34Lyw2gmsGFdMZgDwa5Z0KJjPCU6W8tF6CA-2omAdNzrFaWtaPFpBC0NzYaaB111bKIXxngG97Cnu81deEEKmX-vL-O4tpvUUybuquxrlFvVlTeYlrQqv50_IKsKSYkg-iu1cbqIiWrRq9eTmA6EppmZbqHjKSM5JYFbPB_oZ9QeHKnp1_MTom-jKMEpw18qq-PzdX_skZWQ",
6 | "e": "AQAB",
7 | "use": "sig",
8 | "kid": "localhost-signer",
9 | "alg": "RS256",
10 | "n": "lFTMP9TSUwLua0G8M7foqmdUS2us1-JOF8H_tClVG3IEQMRvMmHJoGSdldWDHsNwRG3Wevl_8fZoGocw9hPqj93j-vI4-ZkbxwhPyRqlS0FNIPD1Ln5R6AmHu7b-paRIz3lvqpyTRwnGBI9weE4u6WOpOQ8DjJMNPq4WcM42AgDJAvc6UuhcWW_MLIsjkKp_VYKxzthSuiRAxXi8Pz4ZhiTAEZI-UN61DYU9YEFNujg5XtIQsRwQn1Vj7BknGwkdf_iCGJgDlKUOz9hAojOMXTAwetUx6I5nngIM5vaXWJCmKn6SzcTYgHWWVrn8qaSazioaydLaYN9NuQ0MdIvsQw"
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/token-validation-ktor-v3/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 |
12 | ### IntelliJ IDEA ###
13 | .idea
14 | *.iws
15 | *.iml
16 | *.ipr
17 |
18 | ### NetBeans ###
19 | nbproject/private/
20 | build/
21 | nbbuild/
22 | dist/
23 | nbdist/
24 | .nb-gradle/
25 |
--------------------------------------------------------------------------------
/token-validation-ktor-v3/src/main/kotlin/no/nav/security/token/support/v3/JwtTokenExpiryThresholdHandler.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.v3
2 |
3 | import com.nimbusds.jwt.JWTClaimNames.EXPIRATION_TIME
4 | import io.ktor.server.application.*
5 | import io.ktor.server.response.*
6 | import java.time.LocalDateTime.now
7 | import java.time.LocalDateTime.ofInstant
8 | import java.time.ZoneId.systemDefault
9 | import java.time.temporal.ChronoUnit.MINUTES
10 | import java.util.*
11 | import no.nav.security.token.support.core.JwtTokenConstants.TOKEN_EXPIRES_SOON_HEADER
12 | import no.nav.security.token.support.core.context.TokenValidationContext
13 | import no.nav.security.token.support.core.jwt.JwtTokenClaims
14 | import org.slf4j.LoggerFactory
15 |
16 | class JwtTokenExpiryThresholdHandler(private val expiryThreshold: Int) {
17 |
18 | private val log = LoggerFactory.getLogger(JwtTokenExpiryThresholdHandler::class.java.name)
19 |
20 | fun addHeaderOnTokenExpiryThreshold(call: ApplicationCall, ctx: TokenValidationContext) {
21 | if(expiryThreshold > 0) {
22 | ctx.issuers.forEach {
23 | if (tokenExpiresBeforeThreshold(ctx.getClaims(it))) {
24 | call.response.header(TOKEN_EXPIRES_SOON_HEADER, "true")
25 | } else {
26 | log.debug("Token is still within expiry threshold.")
27 | }
28 | }
29 | } else {
30 | log.debug("Expiry threshold is not set")
31 | }
32 | }
33 |
34 | private fun tokenExpiresBeforeThreshold(jwtTokenClaims: JwtTokenClaims): Boolean {
35 | val expiryDate = jwtTokenClaims.get(EXPIRATION_TIME) as Date
36 | val expiry = ofInstant(expiryDate.toInstant(), systemDefault())
37 | val minutesUntilExpiry = now().until(expiry, MINUTES)
38 | log.debug("Checking token at time {} with expirationTime {} for how many minutes until expiry: {}",
39 | now(), expiry, minutesUntilExpiry)
40 | if (minutesUntilExpiry <= expiryThreshold) {
41 | log.debug("There are {} minutes until expiry which is equal to or less than the configured threshold {}",
42 | minutesUntilExpiry, expiryThreshold)
43 | return true
44 | }
45 | return false
46 | }
47 | }
--------------------------------------------------------------------------------
/token-validation-ktor-v3/src/test/kotlin/no/nav/security/token/support/v3/JwkGenerator.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.v3
2 |
3 | import com.nimbusds.jose.jwk.JWKSet
4 | import com.nimbusds.jose.jwk.RSAKey
5 | import com.nimbusds.jose.util.IOUtils
6 | import java.io.IOException
7 | import java.nio.charset.StandardCharsets
8 | import java.text.ParseException
9 |
10 |
11 | object JwkGenerator {
12 | const val DEFAULT_KEYID = "localhost-signer"
13 | const val DEFAULT_JWKSET_FILE = "/jwkset.json"
14 | val defaultRSAKey: RSAKey
15 | get() = jWKSet.getKeyByKeyId(DEFAULT_KEYID) as RSAKey
16 |
17 | val jWKSet: JWKSet
18 | get() = try {
19 | JWKSet.parse(
20 | IOUtils.readInputStreamToString(
21 | JwkGenerator::class.java.getResourceAsStream(DEFAULT_JWKSET_FILE), StandardCharsets.UTF_8))
22 | } catch (io: IOException) {
23 | throw RuntimeException(io)
24 | } catch (io: ParseException) {
25 | throw RuntimeException(io)
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/token-validation-ktor-v3/src/test/kotlin/no/nav/security/token/support/v3/JwtTokenGenerator.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.v3
2 |
3 | import com.nimbusds.jose.JOSEException
4 | import com.nimbusds.jose.JOSEObjectType
5 | import com.nimbusds.jose.JWSAlgorithm
6 | import com.nimbusds.jose.JWSHeader
7 | import com.nimbusds.jose.JWSSigner
8 | import com.nimbusds.jose.crypto.RSASSASigner
9 | import com.nimbusds.jose.jwk.RSAKey
10 | import com.nimbusds.jwt.JWTClaimsSet
11 | import com.nimbusds.jwt.JWTClaimsSet.Builder
12 | import com.nimbusds.jwt.SignedJWT
13 | import java.util.*
14 | import java.util.concurrent.TimeUnit.MINUTES
15 |
16 | object JwtTokenGenerator {
17 | const val ISS = "iss-localhost"
18 | const val AUD = "aud-localhost"
19 | const val ACR = "Level4"
20 | const val EXPIRY = (60 * 60 * 3600).toLong()
21 | fun signedJWTAsString(subject: String?): String {
22 | return createSignedJWT(subject).serialize()
23 | }
24 |
25 | @JvmOverloads
26 | fun createSignedJWT(subject: String?, expiryInMinutes: Long = EXPIRY): SignedJWT {
27 | val claimsSet = buildClaimSet(subject, ISS, AUD, ACR, MINUTES.toMillis(expiryInMinutes))
28 | return createSignedJWT(
29 | JwkGenerator.defaultRSAKey,
30 | claimsSet)
31 | }
32 |
33 | fun createSignedJWT(claimsSet: JWTClaimsSet?): SignedJWT {
34 | return createSignedJWT(JwkGenerator.defaultRSAKey, claimsSet)
35 | }
36 |
37 | fun buildClaimSet(subject: String?, issuer: String?, audience: String?, authLevel: String?,
38 | expiry: Long): JWTClaimsSet {
39 | val now = Date()
40 | return Builder()
41 | .subject(subject)
42 | .issuer(issuer)
43 | .audience(audience)
44 | .jwtID(UUID.randomUUID().toString())
45 | .claim("acr", authLevel)
46 | .claim("ver", "1.0")
47 | .claim("nonce", "myNonce")
48 | .claim("auth_time", now)
49 | .notBeforeTime(now)
50 | .issueTime(now)
51 | .expirationTime(Date(now.time + expiry)).build()
52 | }
53 |
54 | fun createSignedJWT(rsaJwk: RSAKey, claimsSet: JWTClaimsSet?): SignedJWT {
55 | return try {
56 | val header = JWSHeader.Builder(JWSAlgorithm.RS256)
57 | .keyID(rsaJwk.keyID)
58 | .type(JOSEObjectType.JWT)
59 | val signedJWT = SignedJWT(header.build(), claimsSet)
60 | val signer: JWSSigner = RSASSASigner(rsaJwk.toPrivateKey())
61 | signedJWT.sign(signer)
62 | signedJWT
63 | } catch (e: JOSEException) {
64 | throw RuntimeException(e)
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/token-validation-ktor-v3/src/test/kotlin/no/nav/security/token/support/v3/TokenSupportAuthenticationProviderKtTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.v3
2 |
3 | import com.nimbusds.jwt.JWTClaimNames.SUBJECT
4 | import io.kotest.assertions.asClue
5 | import io.kotest.matchers.shouldBe
6 | import io.ktor.server.config.*
7 | import no.nav.security.mock.oauth2.withMockOAuth2Server
8 | import no.nav.security.token.support.core.configuration.IssuerProperties
9 | import org.junit.jupiter.api.Test
10 |
11 | internal class TokenSupportAuthenticationProviderKtTest {
12 |
13 | @Test
14 | fun `config properties are parsed correctly`() {
15 | withMockOAuth2Server {
16 | val config = MapApplicationConfig(
17 | "no.nav.security.jwt.expirythreshold" to "5",
18 | "no.nav.security.jwt.issuers.size" to "1",
19 | "no.nav.security.jwt.issuers.0.issuer_name" to "da issuah",
20 | "no.nav.security.jwt.issuers.0.discoveryurl" to this.wellKnownUrl("whatever").toString(),
21 | "no.nav.security.jwt.issuers.0.accepted_audience" to "da audienze",
22 | "no.nav.security.jwt.issuers.0.jwks_cache.lifespan" to "20",
23 | "no.nav.security.jwt.issuers.0.jwks_cache.refreshtime" to "57",
24 | "no.nav.security.jwt.issuers.0.validation.optional_claims" to SUBJECT
25 | )
26 |
27 | config.asIssuerProps().asClue {
28 | it["da issuah"]?.acceptedAudience shouldBe listOf("da audienze")
29 | it["da issuah"]?.discoveryUrl shouldBe this.wellKnownUrl("whatever").toUrl()
30 | it["da issuah"]?.jwksCache shouldBe IssuerProperties.JwksCache(20, 57)
31 | it["da issuah"]?.validation shouldBe IssuerProperties.Validation(listOf(SUBJECT))
32 | }
33 | }
34 | }
35 |
36 |
37 | }
--------------------------------------------------------------------------------
/token-validation-ktor-v3/src/test/kotlin/no/nav/security/token/support/v3/inlineconfigtestapp/InlineConfigApplication.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.v3.inlineconfigtestapp
2 |
3 | import com.nimbusds.jose.util.DefaultResourceRetriever
4 | import io.ktor.http.*
5 | import io.ktor.server.application.*
6 | import io.ktor.server.auth.*
7 | import io.ktor.server.netty.*
8 | import io.ktor.server.response.*
9 | import io.ktor.server.routing.*
10 | import no.nav.security.token.support.core.configuration.ProxyAwareResourceRetriever.Companion.DEFAULT_HTTP_CONNECT_TIMEOUT
11 | import no.nav.security.token.support.core.configuration.ProxyAwareResourceRetriever.Companion.DEFAULT_HTTP_READ_TIMEOUT
12 | import no.nav.security.token.support.core.configuration.ProxyAwareResourceRetriever.Companion.DEFAULT_HTTP_SIZE_LIMIT
13 | import no.nav.security.token.support.v3.IssuerConfig
14 | import no.nav.security.token.support.v3.TokenSupportConfig
15 | import no.nav.security.token.support.v3.tokenValidationSupport
16 |
17 | fun main(args: Array): Unit = EngineMain.main(args)
18 |
19 | var helloCounter = 0
20 |
21 | fun Application.inlineConfiguredModule() {
22 | install(Authentication) {
23 | tokenValidationSupport(config = TokenSupportConfig(IssuerConfig("iss-localhost", "http://localhost:33445/.well-known/openid-configuration", listOf("aud-localhost", "anotherAudience"))), resourceRetriever = DefaultResourceRetriever(DEFAULT_HTTP_CONNECT_TIMEOUT, DEFAULT_HTTP_READ_TIMEOUT, DEFAULT_HTTP_SIZE_LIMIT))
24 | }
25 | routing {
26 | authenticate {
27 | get("/inlineconfig") {
28 | helloCounter++
29 | call.respondText("Authenticated hello with inline config", ContentType.Text.Html)
30 | }
31 | }
32 | }
33 |
34 |
35 | }
--------------------------------------------------------------------------------
/token-validation-ktor-v3/src/test/kotlin/no/nav/security/token/support/v3/testapp/TestApplication.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.v3.testapp
2 |
3 | import io.ktor.http.*
4 | import io.ktor.server.application.*
5 | import io.ktor.server.auth.*
6 | import io.ktor.server.response.*
7 | import io.ktor.server.routing.*
8 | import no.nav.security.token.support.v3.RequiredClaims
9 | import no.nav.security.token.support.v3.TokenValidationContextPrincipal
10 | import no.nav.security.token.support.v3.tokenValidationSupport
11 |
12 | fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args)
13 |
14 | @Suppress("unused") // Referenced in application.conf
15 | fun Application.module() {
16 |
17 | val config = this.environment.config
18 | val acceptedIssuer = "default"
19 |
20 | install(Authentication) {
21 | tokenValidationSupport("validToken", config = config)
22 | tokenValidationSupport("validUser", config = config,
23 | requiredClaims = RequiredClaims(issuer = acceptedIssuer, claimMap = arrayOf("NAVident=X112233"))
24 | )
25 | tokenValidationSupport("validGroup", config = config,
26 | additionalValidation = {
27 | val claims = it.getClaims(acceptedIssuer)
28 | val groups = claims.getAsList("groups")
29 | val hasGroup = groups != null && groups.contains("THEGROUP")
30 | val hasIdentRequiredForAuditLog = claims.getStringClaim("NAVident") != null
31 | hasGroup && hasIdentRequiredForAuditLog
32 | })
33 | }
34 |
35 | routing {
36 | authenticate("validToken") {
37 | get("/hello") {
38 |
39 | call.respondText("Authenticated hello", ContentType.Text.Html)
40 | }
41 | }
42 |
43 | authenticate("validUser") {
44 | get("/hello_person") {
45 | call.respondText("Hello X112233", ContentType.Text.Html)
46 | }
47 | }
48 |
49 | authenticate("validGroup") {
50 | get("/hello_group") {
51 | val principal: TokenValidationContextPrincipal? = call.authentication.principal()
52 | val ident = principal?.context?.getClaims(acceptedIssuer)?.getStringClaim("NAVident")
53 | println("NAVident = $ident is accessing hello_group")
54 | call.respondText("Hello THEGROUP", ContentType.Text.Html)
55 | }
56 | }
57 |
58 | get("/openhello") {
59 | call.respondText("Hello in the open", ContentType.Text.Html)
60 | }
61 |
62 | }
63 |
64 |
65 | }
--------------------------------------------------------------------------------
/token-validation-ktor-v3/src/test/resources/jwkset.json:
--------------------------------------------------------------------------------
1 | {
2 | "keys": [
3 | {
4 | "kty": "RSA",
5 | "d": "MRf73iiXUEhJFxDTtJ5rEHNQsAG8XFuXkz9vXXbMp1_OTo11bEx3SnHiwmO_mSAAeXWNJniLw07V1-nk551h5in_ueAPwXTOf8qddacvDEBZwcxeqfu_Kjh1R0ji8Xn1a037CpH2IO34Lyw2gmsGFdMZgDwa5Z0KJjPCU6W8tF6CA-2omAdNzrFaWtaPFpBC0NzYaaB111bKIXxngG97Cnu81deEEKmX-vL-O4tpvUUybuquxrlFvVlTeYlrQqv50_IKsKSYkg-iu1cbqIiWrRq9eTmA6EppmZbqHjKSM5JYFbPB_oZ9QeHKnp1_MTom-jKMEpw18qq-PzdX_skZWQ",
6 | "e": "AQAB",
7 | "use": "sig",
8 | "kid": "localhost-signer",
9 | "alg": "RS256",
10 | "n": "lFTMP9TSUwLua0G8M7foqmdUS2us1-JOF8H_tClVG3IEQMRvMmHJoGSdldWDHsNwRG3Wevl_8fZoGocw9hPqj93j-vI4-ZkbxwhPyRqlS0FNIPD1Ln5R6AmHu7b-paRIz3lvqpyTRwnGBI9weE4u6WOpOQ8DjJMNPq4WcM42AgDJAvc6UuhcWW_MLIsjkKp_VYKxzthSuiRAxXi8Pz4ZhiTAEZI-UN61DYU9YEFNujg5XtIQsRwQn1Vj7BknGwkdf_iCGJgDlKUOz9hAojOMXTAwetUx6I5nngIM5vaXWJCmKn6SzcTYgHWWVrn8qaSazioaydLaYN9NuQ0MdIvsQw"
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/token-validation-spring-demo/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 |
12 | ### IntelliJ IDEA ###
13 | .idea
14 | *.iws
15 | *.iml
16 | *.ipr
17 |
18 | ### NetBeans ###
19 | /nbproject/private/
20 | /build/
21 | /nbbuild/
22 | /dist/
23 | /nbdist/
24 | /.nb-gradle/
--------------------------------------------------------------------------------
/token-validation-spring-demo/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/navikt/token-support/731015c17f10aa7d9286dea5b0480c1837d7944e/token-validation-spring-demo/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/token-validation-spring-demo/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip
2 |
--------------------------------------------------------------------------------
/token-validation-spring-demo/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | token-validation-spring-demo
6 | jar
7 | token-validation-spring-demo
8 | Demo project for Spring Boot
9 |
10 |
11 | no.nav.security
12 | token-support
13 | 3.0.0-SNAPSHOT
14 |
15 |
16 |
17 |
18 | org.springframework.boot
19 | spring-boot-starter-web
20 |
21 |
22 | org.springframework.boot
23 | spring-boot-starter-jetty
24 |
25 |
26 |
27 |
28 | org.springframework.boot
29 | spring-boot-starter-tomcat
30 |
31 |
32 | no.nav.security
33 | token-validation-spring
34 |
35 |
36 | no.nav.security
37 | token-validation-spring-test
38 |
39 |
40 | org.springframework.boot
41 | spring-boot-starter-validation
42 |
43 |
44 | org.springframework.boot
45 | spring-boot-starter-test
46 | test
47 |
48 |
49 |
50 | ${project.basedir}/src/main/kotlin
51 | ${project.basedir}/src/test/kotlin
52 |
53 |
54 | org.jetbrains.kotlin
55 | kotlin-maven-plugin
56 |
57 |
58 | org.apache.maven.plugins
59 | maven-source-plugin
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/token-validation-spring-demo/src/main/kotlin/no/nav/security/token/support/demo/spring/DemoApplication.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.demo.spring
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class DemoApplication {
8 |
9 | fun main(args : Array) {
10 | runApplication(*args)
11 | }
12 | }
--------------------------------------------------------------------------------
/token-validation-spring-demo/src/main/kotlin/no/nav/security/token/support/demo/spring/config/SecurityConfiguration.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.demo.spring.config
2 |
3 | import no.nav.security.token.support.spring.api.EnableJwtTokenValidation
4 | import org.springframework.context.annotation.Configuration
5 |
6 | @EnableJwtTokenValidation
7 | @Configuration
8 | class SecurityConfiguration
--------------------------------------------------------------------------------
/token-validation-spring-demo/src/main/kotlin/no/nav/security/token/support/demo/spring/rest/DemoController.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.demo.spring.rest
2 |
3 | import no.nav.security.token.support.core.api.Protected
4 | import no.nav.security.token.support.core.api.Unprotected
5 | import org.springframework.web.bind.annotation.GetMapping
6 | import org.springframework.web.bind.annotation.RestController
7 |
8 | @Protected
9 | @RestController
10 | class DemoController {
11 |
12 | @GetMapping("/demo/protected")
13 | fun protectedPath() = "I am protected"
14 |
15 | @Unprotected
16 | @GetMapping("/demo/unprotected")
17 | fun unprotectedPath() = "I am unprotected"
18 | }
--------------------------------------------------------------------------------
/token-validation-spring-demo/src/main/resources/META-INF/additional-spring-configuration-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "properties": [
3 | {
4 | "name": "mock-oauth2-server.port",
5 | "type": "java.lang.String",
6 | "description": "Description for mock-oauth2-server.port."
7 | },
8 | {
9 | "name": "mock-oauth2-server.interactive-login",
10 | "type": "java.lang.String",
11 | "description": "Description for mock-oauth2-server.interactive-login."
12 | }
13 | ] }
--------------------------------------------------------------------------------
/token-validation-spring-demo/src/main/resources/application.yaml:
--------------------------------------------------------------------------------
1 |
2 | http.proxy.parametername: notused
3 |
4 | no.nav.security.jwt:
5 | expirythreshold: 1 #threshold in minutes until token expires
6 | issuer:
7 | issuer1:
8 | discovery-url: https://someissuer/.well-known/openid-configuration
9 | accepted_audience: demoapplication
10 |
11 | logging.level.org.springframework: INFO
12 | logging.level.no.nav: DEBUG
--------------------------------------------------------------------------------
/token-validation-spring-demo/src/test/kotlin/no/nav/security/token/support/demo/spring/LocalDemoApplication.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.demo.spring
2 |
3 | import org.springframework.boot.SpringApplication
4 | import org.springframework.boot.autoconfigure.SpringBootApplication
5 |
6 | @SpringBootApplication
7 | class LocalDemoApplication {
8 |
9 | fun main(args : Array) {
10 | val app = SpringApplication(LocalDemoApplication::class.java)
11 | app.setAdditionalProfiles("local")
12 | app.run(*args)
13 | }
14 | }
--------------------------------------------------------------------------------
/token-validation-spring-demo/src/test/kotlin/no/nav/security/token/support/demo/spring/LocalSecurityConfiguration.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.demo.spring
2 |
3 | import no.nav.security.token.support.spring.test.EnableMockOAuth2Server
4 | import org.springframework.context.annotation.Configuration
5 | import org.springframework.context.annotation.Profile
6 |
7 | @Configuration
8 | @Profile("local")
9 | @EnableMockOAuth2Server
10 | class LocalSecurityConfiguration
--------------------------------------------------------------------------------
/token-validation-spring-demo/src/test/resources/application-local.yaml:
--------------------------------------------------------------------------------
1 |
2 | http.proxy.parametername: notused
3 |
4 | no.nav.security.jwt:
5 | expirythreshold: 1 #threshold in minutes until token expires
6 | issuer:
7 | issuer1:
8 | discovery-url: http://localhost:${mock-oauth2-server.port}/issuer1/.well-known/openid-configuration
9 | accepted_audience: demoapplication
10 | issuer2:
11 | discovery-url: http://localhost:${mock-oauth2-server.port}/issuer2/.well-known/openid-configuration
12 | accepted_audience: demoapplication
13 |
14 | logging.level:
15 | org.springframework: INFO
16 | no.nav: DEBUG
--------------------------------------------------------------------------------
/token-validation-spring-demo/src/test/resources/application-test.yaml:
--------------------------------------------------------------------------------
1 | no.nav.security.jwt:
2 | expirythreshold: 1 #threshold in minutes until token expires
3 | issuer:
4 | issuer1:
5 | discovery-url: http://localhost:${mock-oauth2-server.port}/issuer1/.well-known/openid-configuration
6 | accepted_audience: demoapplication
7 | issuer2:
8 | discovery-url: http://localhost:${mock-oauth2-server.port}/issuer2/.well-known/openid-configuration
9 | accepted_audience: demoapplication
10 |
11 | logging:
12 | level:
13 | org.springframework: INFO
14 | no.nav: DEBUG
--------------------------------------------------------------------------------
/token-validation-spring-test/README.md:
--------------------------------------------------------------------------------
1 | # token-validation-spring-test
2 |
3 | Contains Spring auto-configuration for setting up a [mock-oauth2-server](https://github.com/navikt/mock-oauth2-server).
4 |
5 | The mock-oauth2-server can be used to represent as many issuers as you'd like supporting OpenID Connect/OAuth 2.0 discovery
6 | with valid JWKS uris, and should work nicely together with the validation from [token-validation-spring](../token-validation-spring)
7 |
8 | ## Configuration for local setup
9 |
10 | - Add as dependency with **test** scope
11 |
12 | - Simply add the annotation `@EnableMockOAuth2Server` to your test configuration:
13 |
14 | ```java
15 | @EnableMockOAuth2Server
16 | ```
17 |
18 | - For usage with token-validation-spring set the following properties in your local profile:
19 |
20 | `no.nav.security.jwt.issuer.issuer1.discoveryurl=http://localhost:${mock-oauth2-server.port}/issuer1/.well-known/openid-configuration`
21 |
22 | `no.nav.security.jwt.issuer.issuer1.acceptedaudience=someaudience`
23 |
24 |
25 | - For local use of your app there should now be RestController available in your app at **/local**
26 |
27 | The query param `issuerId` must match the path after port in the `discoveryurl` - e.g. `issuer1` in `http://localhost:${mock-oauth2-server.port}/issuer1/.well-known/openid-configuration`
28 |
29 | ## How to use
30 |
31 | See [token-validation-spring-demo](../token-validation-spring-demo) for usage scenarios when starting your app locally.
32 |
33 | For **JUnit** tests, your Spring application context should contain a bean of the type `MockOAuth2Server` which can be used to issue tokens and provides a JWKS endpoint for validation.
34 | * Usage: [DemoControllerTest.java](../token-validation-spring-demo/src/test/java/no/nav/security/token/support/demo/spring/rest/DemoControllerTest.java)
35 | * For detailed usage and features see the [mock-oauth2-server](https://github.com/navikt/mock-oauth2-server) documentation.
--------------------------------------------------------------------------------
/token-validation-spring-test/src/main/kotlin/no/nav/security/token/support/spring/test/EnableMockOAuth2Server.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.test
2 |
3 | import java.lang.annotation.Inherited
4 | import org.springframework.boot.test.autoconfigure.properties.PropertyMapping
5 | import org.springframework.context.annotation.Import
6 | import kotlin.annotation.AnnotationRetention.RUNTIME
7 | import kotlin.annotation.AnnotationTarget.CLASS
8 |
9 | @MustBeDocumented
10 | @Inherited
11 | @Retention(RUNTIME)
12 | @Target(CLASS)
13 | @Import(MockOAuth2ServerAutoConfiguration::class, MockLoginController::class)
14 | @PropertyMapping(MockOAuth2ServerProperties.PREFIX)
15 | annotation class EnableMockOAuth2Server(
16 | /**
17 | * Specify port for server to run on (only works in test scope), provide via
18 | * env property mock-ouath2-server.port outside of test scope
19 | */
20 | val port : Int = 0)
--------------------------------------------------------------------------------
/token-validation-spring-test/src/main/kotlin/no/nav/security/token/support/spring/test/MockLoginController.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.test
2 |
3 | import no.nav.security.mock.oauth2.MockOAuth2Server
4 | import org.springframework.web.bind.annotation.RequestMapping
5 | import org.springframework.web.bind.annotation.RestController
6 |
7 | @RestController
8 | @RequestMapping("/local")
9 | class MockLoginController(private val mockOAuth2Server : MockOAuth2Server)
--------------------------------------------------------------------------------
/token-validation-spring-test/src/main/kotlin/no/nav/security/token/support/spring/test/MockOAuth2ServerAutoConfiguration.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.test
2 |
3 | import jakarta.annotation.PostConstruct
4 | import jakarta.annotation.PreDestroy
5 | import org.slf4j.Logger
6 | import org.slf4j.LoggerFactory
7 | import org.springframework.boot.context.properties.ConfigurationProperties
8 | import org.springframework.boot.context.properties.EnableConfigurationProperties
9 | import org.springframework.context.annotation.Bean
10 | import org.springframework.context.annotation.Configuration
11 | import org.springframework.context.annotation.DependsOn
12 | import org.springframework.context.annotation.Primary
13 | import no.nav.security.mock.oauth2.MockOAuth2Server
14 | import no.nav.security.mock.oauth2.OAuth2Config
15 | import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback
16 | import no.nav.security.mock.oauth2.token.OAuth2TokenProvider
17 | import no.nav.security.token.support.core.configuration.ProxyAwareResourceRetriever
18 |
19 | @Configuration
20 | @EnableConfigurationProperties(MockOAuth2ServerProperties::class)
21 | class MockOAuth2ServerAutoConfiguration(private val properties : MockOAuth2ServerProperties) {
22 |
23 | private val log : Logger = LoggerFactory.getLogger(MockOAuth2ServerAutoConfiguration::class.java)
24 | private val mockOAuth2Server = MockOAuth2Server(OAuth2Config(properties.isInteractiveLogin, null, null,false,OAuth2TokenProvider(), setOf(DefaultOAuth2TokenCallback())))
25 |
26 | @Bean
27 | @Primary
28 | @DependsOn("mockOAuth2Server")
29 | fun overrideOidcResourceRetriever() = ProxyAwareResourceRetriever()
30 |
31 | @Bean
32 | fun mockOAuth2Server() = mockOAuth2Server
33 |
34 | @PostConstruct
35 | fun start() {
36 | log.debug("starting the mock oauth2 server on {}.port", properties)
37 | mockOAuth2Server.start(properties.port)
38 | }
39 |
40 | @PreDestroy
41 | fun shutdown() {
42 | log.debug("shutting down the mock oauth2 server.")
43 | mockOAuth2Server.shutdown()
44 | }
45 | }
46 |
47 | @ConfigurationProperties(MockOAuth2ServerProperties.PREFIX)
48 | class MockOAuth2ServerProperties(val port : Int, val isInteractiveLogin : Boolean = false) {
49 |
50 | init {
51 | require(port > 0) { "port must be set" }
52 | }
53 |
54 | companion object {
55 |
56 | const val PREFIX : String = "mock-oauth2-server"
57 | }
58 | }
--------------------------------------------------------------------------------
/token-validation-spring-test/src/main/resources/META-INF/spring.factories:
--------------------------------------------------------------------------------
1 | # Application Listeners
2 | org.springframework.context.ApplicationListener=\
3 | no.nav.security.token.support.spring.test.MockOAuth2ServerApplicationListener
--------------------------------------------------------------------------------
/token-validation-spring-test/src/test/kotlin/no/nav/security/token/support/spring/test/EnableMockOAuth2ServerRandomPortTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.test
2 |
3 |
4 | import org.assertj.core.api.Assertions.assertThat
5 | import org.junit.jupiter.api.Assertions.assertEquals
6 | import org.junit.jupiter.api.Test
7 | import org.springframework.beans.factory.annotation.Autowired
8 | import org.springframework.beans.factory.annotation.Value
9 | import org.springframework.boot.test.context.SpringBootTest
10 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE
11 | import no.nav.security.mock.oauth2.MockOAuth2Server
12 |
13 | @SpringBootTest(classes = [TestApplication::class], properties = ["discoveryUrl=http://localhost:\${mock-oauth2-server.port}/test/.well-known/openid-configuration"], webEnvironment = NONE)
14 | @EnableMockOAuth2Server
15 | internal class EnableMockOAuth2ServerRandomPortTest {
16 |
17 | @Autowired
18 | private lateinit var properties : MockOAuth2ServerProperties
19 |
20 | @Autowired
21 | private lateinit var server : MockOAuth2Server
22 |
23 | @Value("\${discoveryUrl}")
24 | private lateinit var discoveryUrl : String
25 |
26 | @Test
27 | fun serverStartsOnRandomPortAndIsUpdatedInEnv() {
28 | assertEquals(server.baseUrl().port,properties.port)
29 | assertThat(server.wellKnownUrl("test")).hasToString(discoveryUrl)
30 | }
31 | }
--------------------------------------------------------------------------------
/token-validation-spring-test/src/test/kotlin/no/nav/security/token/support/spring/test/EnableMockOAuth2ServerRandomStaticPortTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.test
2 |
3 | import org.assertj.core.api.Assertions.assertThat
4 | import org.junit.jupiter.api.Assertions.assertEquals
5 | import org.junit.jupiter.api.Test
6 | import org.springframework.beans.factory.annotation.Autowired
7 | import org.springframework.beans.factory.annotation.Value
8 | import org.springframework.boot.test.context.SpringBootTest
9 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE
10 | import no.nav.security.mock.oauth2.MockOAuth2Server
11 |
12 | @SpringBootTest(classes = [TestApplication::class], properties = ["discoveryUrl=http://localhost:\${mock-oauth2-server.port}/test/.well-known/openid-configuration"], webEnvironment = NONE)
13 | @EnableMockOAuth2Server(port = 1234)
14 | internal class EnableMockOAuth2ServerRandomStaticPortTest {
15 |
16 | @Autowired
17 | private lateinit var server : MockOAuth2Server
18 |
19 | @Value("\${discoveryUrl}")
20 | private lateinit var discoveryUrl : String
21 |
22 | @Test
23 | fun serverStartsOnStaticPortAndIsUpdatedInEnv() {
24 | assertEquals(1234,server.baseUrl().port)
25 | assertThat(server.wellKnownUrl("test")).hasToString(discoveryUrl)
26 | }
27 | }
--------------------------------------------------------------------------------
/token-validation-spring-test/src/test/kotlin/no/nav/security/token/support/spring/test/TestApplication.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.test
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 | @SpringBootApplication
6 | class TestApplication {
7 |
8 | fun main(args : Array) {
9 | runApplication(*args)
10 | }
11 | }
--------------------------------------------------------------------------------
/token-validation-spring/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 |
12 | ### IntelliJ IDEA ###
13 | .idea
14 | *.iws
15 | *.iml
16 | *.ipr
17 |
18 | ### NetBeans ###
19 | nbproject/private/
20 | build/
21 | nbbuild/
22 | dist/
23 | nbdist/
24 | .nb-gradle/
25 |
--------------------------------------------------------------------------------
/token-validation-spring/src/main/kotlin/no/nav/security/token/support/spring/MultiIssuerProperties.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring
2 |
3 | import jakarta.validation.Valid
4 | import no.nav.security.token.support.core.configuration.IssuerProperties
5 | import org.springframework.boot.context.properties.ConfigurationProperties
6 | import org.springframework.boot.context.properties.EnableConfigurationProperties
7 | import org.springframework.validation.annotation.Validated
8 |
9 | @ConfigurationProperties("no.nav.security.jwt")
10 | @EnableConfigurationProperties
11 | @Validated
12 | data class MultiIssuerProperties(@Valid val issuer: Map = emptyMap())
--------------------------------------------------------------------------------
/token-validation-spring/src/main/kotlin/no/nav/security/token/support/spring/ProtectedRestController.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring
2 |
3 | import no.nav.security.token.support.core.api.ProtectedWithClaims
4 | import no.nav.security.token.support.core.api.Unprotected
5 | import org.springframework.core.annotation.AliasFor
6 | import org.springframework.http.MediaType.APPLICATION_JSON_VALUE
7 | import org.springframework.http.MediaType.TEXT_PLAIN_VALUE
8 | import org.springframework.web.bind.annotation.RequestMapping
9 | import org.springframework.web.bind.annotation.RestController
10 | import kotlin.annotation.AnnotationRetention.RUNTIME
11 | import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
12 | import kotlin.annotation.AnnotationTarget.CLASS
13 |
14 | @RestController
15 | @MustBeDocumented
16 | @ProtectedWithClaims(issuer = "must-be-set-to-issuer-short-name")
17 | @Target(ANNOTATION_CLASS, CLASS)
18 | @Retention(RUNTIME)
19 | @RequestMapping
20 | annotation class ProtectedRestController(@get: AliasFor(annotation = ProtectedWithClaims::class, attribute = "issuer") val issuer: String,
21 | @get: AliasFor(annotation = ProtectedWithClaims::class, attribute = "claimMap") val claimMap: Array = ["acr=Level4"],
22 | @get: AliasFor(annotation = RequestMapping::class, attribute = "value") val value: Array = ["/"],
23 | @get: AliasFor(annotation = RequestMapping::class, attribute = "produces") val produces: Array = [APPLICATION_JSON_VALUE])
24 |
25 | @RestController
26 | @MustBeDocumented
27 | @Unprotected
28 | @Target(ANNOTATION_CLASS, CLASS)
29 | @Retention(RUNTIME)
30 | @RequestMapping
31 | annotation class UnprotectedRestController(@get: AliasFor(annotation = RequestMapping::class, attribute = "value") val value: Array = ["/"],
32 | @get: AliasFor(annotation = RequestMapping::class, attribute = "produces") val produces: Array = [APPLICATION_JSON_VALUE])
--------------------------------------------------------------------------------
/token-validation-spring/src/main/kotlin/no/nav/security/token/support/spring/SpringTokenValidationContextHolder.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring
2 |
3 | import no.nav.security.token.support.core.context.TokenValidationContext
4 | import no.nav.security.token.support.core.context.TokenValidationContextHolder
5 | import org.springframework.web.context.request.RequestAttributes.SCOPE_REQUEST
6 | import org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes
7 |
8 | class SpringTokenValidationContextHolder : TokenValidationContextHolder {
9 |
10 | private val TOKEN_VALIDATION_CONTEXT_ATTRIBUTE = SpringTokenValidationContextHolder::class.java.name
11 | override fun getTokenValidationContext() = getRequestAttribute(TOKEN_VALIDATION_CONTEXT_ATTRIBUTE)?.let { it as TokenValidationContext } ?: TokenValidationContext(emptyMap())
12 | override fun setTokenValidationContext(tokenValidationContext: TokenValidationContext?) = setRequestAttribute(TOKEN_VALIDATION_CONTEXT_ATTRIBUTE, tokenValidationContext)
13 | private fun getRequestAttribute(name: String) = currentRequestAttributes().getAttribute(name, SCOPE_REQUEST)
14 | private fun setRequestAttribute(name: String, value: Any?) = value?.let { currentRequestAttributes().setAttribute(name, it, SCOPE_REQUEST) } ?: currentRequestAttributes().removeAttribute(name, SCOPE_REQUEST)
15 | }
--------------------------------------------------------------------------------
/token-validation-spring/src/main/kotlin/no/nav/security/token/support/spring/api/EnableJwtTokenValidation.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.api
2 |
3 | import java.lang.annotation.Inherited
4 | import no.nav.security.token.support.spring.EnableJwtTokenValidationConfiguration
5 | import org.springframework.context.annotation.Import
6 | import kotlin.annotation.AnnotationRetention.RUNTIME
7 | import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
8 | import kotlin.annotation.AnnotationTarget.CLASS
9 |
10 | @MustBeDocumented
11 | @Inherited
12 | @Retention(RUNTIME)
13 | @Target(ANNOTATION_CLASS, CLASS)
14 | @Import(EnableJwtTokenValidationConfiguration::class)
15 | annotation class EnableJwtTokenValidation(val ignore: Array = ["org.springframework"])
--------------------------------------------------------------------------------
/token-validation-spring/src/main/kotlin/no/nav/security/token/support/spring/validation/interceptor/BearerTokenClientHttpRequestInterceptor.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.validation.interceptor
2 |
3 | import java.io.IOException
4 | import no.nav.security.token.support.core.JwtTokenConstants.AUTHORIZATION_HEADER
5 | import no.nav.security.token.support.core.context.TokenValidationContextHolder
6 | import org.slf4j.LoggerFactory
7 | import org.springframework.http.HttpRequest
8 | import org.springframework.http.client.ClientHttpRequestExecution
9 | import org.springframework.http.client.ClientHttpRequestInterceptor
10 | import org.springframework.http.client.ClientHttpResponse
11 |
12 | class BearerTokenClientHttpRequestInterceptor(private val holder: TokenValidationContextHolder) : ClientHttpRequestInterceptor {
13 | private val log = LoggerFactory.getLogger(BearerTokenClientHttpRequestInterceptor::class.java)
14 |
15 | @Throws(IOException::class)
16 | override fun intercept(req: HttpRequest, body: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse {
17 | holder.getTokenValidationContext().apply {
18 | if (hasValidToken()) {
19 | log.debug("Adding tokens to Authorization header")
20 | req.headers.add(AUTHORIZATION_HEADER, issuers.joinToString { "${getJwtToken(it)?.asBearer()}" })
21 | }
22 | return execution.execute(req, body)
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/token-validation-spring/src/main/kotlin/no/nav/security/token/support/spring/validation/interceptor/JwtTokenHandlerInterceptor.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.validation.interceptor
2 |
3 | import jakarta.servlet.http.HttpServletRequest
4 | import jakarta.servlet.http.HttpServletResponse
5 | import java.util.concurrent.ConcurrentHashMap
6 | import no.nav.security.token.support.core.exceptions.AnnotationRequiredException
7 | import no.nav.security.token.support.core.validation.JwtTokenAnnotationHandler
8 | import org.slf4j.LoggerFactory
9 | import org.springframework.core.annotation.AnnotationAttributes
10 | import org.springframework.http.HttpStatus.NOT_IMPLEMENTED
11 | import org.springframework.web.method.HandlerMethod
12 | import org.springframework.web.server.ResponseStatusException
13 | import org.springframework.web.servlet.HandlerInterceptor
14 |
15 | class JwtTokenHandlerInterceptor(attrs: AnnotationAttributes?, private val h: JwtTokenAnnotationHandler) : HandlerInterceptor {
16 | private val log = LoggerFactory.getLogger(JwtTokenHandlerInterceptor::class.java)
17 | private val handlerFlags: MutableMap = ConcurrentHashMap()
18 | private val ignoreConfig = attrs?.getStringArray("ignore") ?: arrayOfNulls(0) ?: arrayOfNulls(0)
19 |
20 | override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
21 | if (handler is HandlerMethod) {
22 | return if (shouldIgnore(handler.bean)) {
23 | true
24 | }
25 | else try {
26 | h.assertValidAnnotation(handler.method)
27 | } catch (e: AnnotationRequiredException) {
28 | log.warn("Received AnnotationRequiredException from JwtTokenAnnotationHandler. return status=$NOT_IMPLEMENTED", e)
29 | throw ResponseStatusException(NOT_IMPLEMENTED, "Endpoint not accessible")
30 | } catch (e: Exception) {
31 | throw JwtTokenUnauthorizedException(cause = e)
32 | }
33 | }
34 | log.debug("Handler is of type ${handler.javaClass.simpleName}, allowing unprotected access to the resources it accesses")
35 | return true
36 | }
37 |
38 | private fun shouldIgnore(o: Any): Boolean {
39 | val fullName = o.javaClass.name
40 | val ignore = ignoreConfig.any { fullName.startsWith(it) }
41 | log.trace("Adding $fullName to OIDC validation ${if (ignore) "ignore" else "interceptor"} list")
42 | handlerFlags[o] = ignore
43 | return ignore
44 | }
45 | }
--------------------------------------------------------------------------------
/token-validation-spring/src/main/kotlin/no/nav/security/token/support/spring/validation/interceptor/JwtTokenUnauthorizedException.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.validation.interceptor
2 |
3 | import org.springframework.http.HttpStatus.UNAUTHORIZED
4 | import org.springframework.web.bind.annotation.ResponseStatus
5 |
6 | @ResponseStatus(UNAUTHORIZED)
7 | class JwtTokenUnauthorizedException (msg: String? = null, cause: Throwable? = null): RuntimeException(msg,cause)
--------------------------------------------------------------------------------
/token-validation-spring/src/main/kotlin/no/nav/security/token/support/spring/validation/interceptor/SpringJwtTokenAnnotationHandler.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.validation.interceptor
2 |
3 | import java.lang.reflect.AnnotatedElement
4 | import java.lang.reflect.Method
5 | import no.nav.security.token.support.core.context.TokenValidationContextHolder
6 | import no.nav.security.token.support.core.validation.JwtTokenAnnotationHandler
7 | import org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation
8 | import kotlin.reflect.KClass
9 |
10 | class SpringJwtTokenAnnotationHandler(holder: TokenValidationContextHolder) : JwtTokenAnnotationHandler(holder) {
11 | override fun getAnnotation(method: Method, types: List>) =
12 | findAnnotation(method, types) ?: findAnnotation(method.declaringClass, types)
13 |
14 | private fun findAnnotation(e: AnnotatedElement, types: List>) =
15 | types.firstNotNullOfOrNull { findMergedAnnotation(e, it.java) }
16 | }
--------------------------------------------------------------------------------
/token-validation-spring/src/test/kotlin/no/nav/security/token/support/spring/MultiIssuerConfigurationPropertiesTest.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring
2 |
3 | import com.nimbusds.jwt.JWTClaimNames.AUDIENCE
4 | import com.nimbusds.jwt.JWTClaimNames.SUBJECT
5 | import org.assertj.core.api.Assertions.assertThat
6 | import org.junit.jupiter.api.Assertions.assertEquals
7 | import org.junit.jupiter.api.Assertions.assertFalse
8 | import org.junit.jupiter.api.Assertions.assertTrue
9 | import org.junit.jupiter.api.Test
10 | import org.junit.jupiter.api.extension.ExtendWith
11 | import org.springframework.beans.factory.annotation.Autowired
12 | import org.springframework.boot.context.properties.EnableConfigurationProperties
13 | import org.springframework.test.context.TestPropertySource
14 | import org.springframework.test.context.junit.jupiter.SpringExtension
15 |
16 | @TestPropertySource(locations = ["/issuers.properties"])
17 | @ExtendWith(SpringExtension::class)
18 | @EnableConfigurationProperties(MultiIssuerProperties::class)
19 | class MultiIssuerConfigurationPropertiesTest {
20 |
21 | @Autowired
22 | private lateinit var config: MultiIssuerProperties
23 | @Test
24 | fun test() {
25 | assertFalse(config.issuer.isEmpty())
26 | assertTrue(config.issuer.containsKey("number1"))
27 | assertEquals("http://metadata", "${config.issuer["number1"]?.discoveryUrl}")
28 | assertTrue(config.issuer["number1"]!!.acceptedAudience.contains("aud1"))
29 | assertTrue(config.issuer.containsKey("number2"))
30 | assertEquals("http://metadata2", "${config.issuer["number2"]?.discoveryUrl}")
31 | assertTrue(config.issuer["number2"]!!.acceptedAudience.contains("aud2"))
32 | assertTrue(config.issuer.containsKey("number3"))
33 | assertEquals("http://metadata3", "${config.issuer["number3"]?.discoveryUrl}")
34 | assertTrue(config.issuer["number3"]!!.acceptedAudience.contains("aud3") && config.issuer["number3"]!!.acceptedAudience.contains("aud4"))
35 | assertTrue(config.issuer.containsKey("number4"))
36 | assertEquals("http://metadata4", "${config.issuer["number4"]?.discoveryUrl}")
37 | assertThat(config.issuer["number4"]?.validation?.optionalClaims).containsExactly(SUBJECT, AUDIENCE)
38 | assertTrue(config.issuer.containsKey("number5"))
39 | assertEquals("http://metadata5", config.issuer["number5"]!!.discoveryUrl.toString())
40 | assertEquals(15L, config.issuer["number5"]?.jwksCache?.lifespan)
41 | assertEquals(5L, config.issuer["number5"]?.jwksCache?.refreshTime)
42 | }
43 | }
--------------------------------------------------------------------------------
/token-validation-spring/src/test/kotlin/no/nav/security/token/support/spring/integrationtest/JWKGenerator.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.integrationtest
2 |
3 | import com.nimbusds.jose.jwk.RSAKey.Builder
4 | import java.security.KeyPair
5 | import java.security.KeyPairGenerator
6 | import java.security.interfaces.RSAPrivateKey
7 | import java.security.interfaces.RSAPublicKey
8 |
9 | object JwkGenerator {
10 | const val DEFAULT_KEYID = "localhost-signer"
11 |
12 | fun generateKeyPair() =
13 | run {
14 | KeyPairGenerator.getInstance("RSA").apply {
15 | initialize(2048)
16 | }.genKeyPair()
17 | }
18 |
19 | fun createJWK(keyID: String, keyPair: KeyPair) =
20 | Builder(keyPair.public as RSAPublicKey)
21 | .privateKey(keyPair.private as RSAPrivateKey)
22 | .keyID(keyID)
23 | .build()
24 | }
--------------------------------------------------------------------------------
/token-validation-spring/src/test/kotlin/no/nav/security/token/support/spring/integrationtest/JWTTokenGenerator.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.integrationtest
2 |
3 | import com.nimbusds.jose.JOSEObjectType.JWT
4 | import com.nimbusds.jose.JWSAlgorithm.RS256
5 | import com.nimbusds.jose.JWSHeader.Builder
6 | import com.nimbusds.jose.crypto.RSASSASigner
7 | import com.nimbusds.jose.jwk.RSAKey
8 | import com.nimbusds.jwt.JWTClaimsSet
9 | import com.nimbusds.jwt.SignedJWT
10 |
11 | object JwtTokenGenerator {
12 | const val AUD = "aud-localhost"
13 | const val ACR = "Level4"
14 |
15 | fun createSignedJWT(rsaJwk: RSAKey, claimsSet: JWTClaimsSet?) =
16 | SignedJWT(Builder(RS256)
17 | .keyID(rsaJwk.keyID)
18 | .type(JWT).build(), claimsSet).apply {
19 | sign(RSASSASigner(rsaJwk.toPrivateKey()))
20 | }
21 | }
--------------------------------------------------------------------------------
/token-validation-spring/src/test/kotlin/no/nav/security/token/support/spring/integrationtest/ProtectedApplication.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.integrationtest
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class ProtectedApplication {
8 | fun main(args: Array) {
9 | runApplication(*args)
10 | }
11 | }
--------------------------------------------------------------------------------
/token-validation-spring/src/test/kotlin/no/nav/security/token/support/spring/integrationtest/ProtectedApplicationConfig.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.integrationtest
2 |
3 | import no.nav.security.mock.oauth2.MockOAuth2Server
4 | import no.nav.security.token.support.core.configuration.ProxyAwareResourceRetriever
5 | import no.nav.security.token.support.spring.MultiIssuerProperties
6 | import no.nav.security.token.support.spring.api.EnableJwtTokenValidation
7 | import org.springframework.boot.context.properties.EnableConfigurationProperties
8 | import org.springframework.context.annotation.Bean
9 | import org.springframework.context.annotation.Configuration
10 | import org.springframework.context.annotation.DependsOn
11 | import org.springframework.context.annotation.Primary
12 |
13 | @EnableJwtTokenValidation
14 | @EnableConfigurationProperties(MultiIssuerProperties::class)
15 | @Configuration
16 | class ProtectedApplicationConfig {
17 | @Bean
18 | @Primary
19 | @DependsOn("mockOAuth2Server")
20 | fun oidcResourceRetriever() = ProxyAwareResourceRetriever()
21 |
22 |
23 | @Bean
24 | fun mockOAuth2Server() =
25 | MockOAuth2Server().apply {
26 | start(1111)
27 | }
28 | }
--------------------------------------------------------------------------------
/token-validation-spring/src/test/kotlin/no/nav/security/token/support/spring/validation/interceptor/MetaAnnotations.kt:
--------------------------------------------------------------------------------
1 | package no.nav.security.token.support.spring.validation.interceptor
2 |
3 | import no.nav.security.token.support.core.api.Protected
4 | import no.nav.security.token.support.core.api.ProtectedWithClaims
5 | import no.nav.security.token.support.core.api.Unprotected
6 | import kotlin.annotation.AnnotationRetention.RUNTIME
7 | import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
8 | import kotlin.annotation.AnnotationTarget.CLASS
9 | import kotlin.annotation.AnnotationTarget.FUNCTION
10 | import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
11 | import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
12 |
13 | @Protected
14 | @Target(ANNOTATION_CLASS, CLASS, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
15 | @Retention(RUNTIME)
16 | internal annotation class ProtectedMeta
17 |
18 | @ProtectedWithClaims(issuer = "issuer1", claimMap = ["acr=Level4"])
19 | @Target(ANNOTATION_CLASS, CLASS, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
20 | @Retention(RUNTIME)
21 | internal annotation class ProtectedWithClaimsMeta
22 |
23 | @Unprotected
24 | @Target(ANNOTATION_CLASS, CLASS, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
25 | @Retention(RUNTIME)
26 | internal annotation class UnprotectedMeta
--------------------------------------------------------------------------------
/token-validation-spring/src/test/resources/application.yaml:
--------------------------------------------------------------------------------
1 | spring.main.allow-bean-definition-overriding: true
2 | http.proxy.parametername: notused
3 |
4 | no.nav.security.jwt:
5 | issuer:
6 | knownissuer:
7 | discovery-url: http://localhost:1111/knownissuer/.well-known/openid-configuration
8 | accepted-audience: aud-localhost
9 | knownissuer2:
10 | discovery-url: http://localhost:1111/knownissuer2/.well-known/openid-configuration
11 | validation:
12 | optional-claims: sub,aud
13 | knownissuer3:
14 | discovery-url: http://localhost:1111/knownissuer3/.well-known/openid-configuration
15 | accepted-audience: aud-localhost
16 | jwks-cache:
17 | lifespan: 10
18 | refresh-time: 2
--------------------------------------------------------------------------------
/token-validation-spring/src/test/resources/issuers.properties:
--------------------------------------------------------------------------------
1 | no.nav.security.jwt.issuer.number1.discoveryurl=http://metadata
2 | no.nav.security.jwt.issuer.number1.accepted_audience=aud1
3 | no.nav.security.jwt.issuer.number2.discoveryurl=http://metadata2
4 | no.nav.security.jwt.issuer.number2.accepted_audience=aud2
5 | no.nav.security.jwt.issuer.number3.discoveryurl=http://metadata3
6 | no.nav.security.jwt.issuer.number3.accepted_audience=aud3,aud4
7 | no.nav.security.jwt.issuer.number4.discoveryurl=http://metadata4
8 | no.nav.security.jwt.issuer.number4.validation.optionalclaims=sub,aud
9 | no.nav.security.jwt.issuer.number5.discoveryurl=http://metadata5
--------------------------------------------------------------------------------
/token-validation-spring/src/test/resources/jwtkeystore.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/navikt/token-support/731015c17f10aa7d9286dea5b0480c1837d7944e/token-validation-spring/src/test/resources/jwtkeystore.jks
--------------------------------------------------------------------------------