├── .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 --------------------------------------------------------------------------------