├── .github └── workflows │ ├── codeql-analysis.yml │ ├── gradle.yml │ └── security.yml ├── .gitignore ├── .snyk ├── AuthServerRequests.postman_collection.json ├── LICENSE ├── README.md ├── SECURITY.md ├── build.gradle ├── dependency-check-suppressions.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── docs └── asciidoc │ └── index.adoc ├── main ├── java │ └── com │ │ └── example │ │ └── authorizationserver │ │ ├── AuthorizationServerApplication.java │ │ ├── DataInitializer.java │ │ ├── admin │ │ └── web │ │ │ └── AdminWebController.java │ │ ├── config │ │ ├── AuditingConfiguration.java │ │ ├── AuthorizationServerConfigurationProperties.java │ │ ├── IdGeneratorConfiguration.java │ │ ├── PasswordEncoderConfiguration.java │ │ └── WebSecurityConfiguration.java │ │ ├── jwks │ │ ├── JwksEndpoint.java │ │ └── JwtPki.java │ │ ├── oauth │ │ ├── client │ │ │ ├── RegisteredClientService.java │ │ │ ├── api │ │ │ │ ├── RegisteredClientApiController.java │ │ │ │ └── resource │ │ │ │ │ ├── ModifyRegisteredClientResource.java │ │ │ │ │ └── RegisteredClientResource.java │ │ │ ├── dao │ │ │ │ └── RegisteredClientRepository.java │ │ │ ├── model │ │ │ │ ├── AccessTokenFormat.java │ │ │ │ └── RegisteredClient.java │ │ │ └── web │ │ │ │ └── RegisteredClientController.java │ │ ├── common │ │ │ ├── AuthenticationUtil.java │ │ │ ├── ClientCredentials.java │ │ │ ├── GrantType.java │ │ │ └── ResponseType.java │ │ ├── endpoint │ │ │ ├── AuthorizationEndpoint.java │ │ │ ├── introspection │ │ │ │ ├── IntrospectionEndpoint.java │ │ │ │ └── resource │ │ │ │ │ ├── IntrospectionRequest.java │ │ │ │ │ └── IntrospectionResponse.java │ │ │ ├── revocation │ │ │ │ ├── RevocationEndpoint.java │ │ │ │ └── resource │ │ │ │ │ ├── RevocationRequest.java │ │ │ │ │ └── RevocationResponse.java │ │ │ └── token │ │ │ │ ├── AuthorizationCodeTokenEndpointService.java │ │ │ │ ├── ClientCredentialsTokenEndpointService.java │ │ │ │ ├── PasswordTokenEndpointService.java │ │ │ │ ├── RefreshTokenEndpointService.java │ │ │ │ ├── TokenEndpoint.java │ │ │ │ ├── TokenEndpointHelper.java │ │ │ │ └── resource │ │ │ │ ├── TokenRequest.java │ │ │ │ └── TokenResponse.java │ │ ├── pkce │ │ │ ├── CodeChallengeError.java │ │ │ ├── ProofKeyForCodeExchangeVerifier.java │ │ │ └── ProofKeyForCodeExchangeVerifierStandardImpl.java │ │ └── store │ │ │ ├── AuthorizationCode.java │ │ │ └── AuthorizationCodeService.java │ │ ├── oidc │ │ ├── common │ │ │ └── Scope.java │ │ └── endpoint │ │ │ ├── discovery │ │ │ ├── Discovery.java │ │ │ └── DiscoveryEndpoint.java │ │ │ └── userinfo │ │ │ ├── UserInfo.java │ │ │ └── UserInfoEndpoint.java │ │ ├── scim │ │ ├── api │ │ │ ├── ScimGroupRestController.java │ │ │ ├── ScimUserRestController.java │ │ │ └── resource │ │ │ │ ├── AddressResource.java │ │ │ │ ├── CreateScimUserResource.java │ │ │ │ ├── ScimAddressResource.java │ │ │ │ ├── ScimEmailResource.java │ │ │ │ ├── ScimGroupListResource.java │ │ │ │ ├── ScimGroupResource.java │ │ │ │ ├── ScimImsResource.java │ │ │ │ ├── ScimMetaResource.java │ │ │ │ ├── ScimPhoneNumberResource.java │ │ │ │ ├── ScimPhotoResource.java │ │ │ │ ├── ScimRefResource.java │ │ │ │ ├── ScimResource.java │ │ │ │ ├── ScimUserListResource.java │ │ │ │ ├── ScimUserResource.java │ │ │ │ └── mapper │ │ │ │ ├── CreateScimUserResourceMapper.java │ │ │ │ ├── ScimGroupListResourceMapper.java │ │ │ │ ├── ScimGroupResourceMapper.java │ │ │ │ ├── ScimUserListResourceMapper.java │ │ │ │ └── ScimUserResourceMapper.java │ │ ├── dao │ │ │ ├── ScimGroupEntityRepository.java │ │ │ ├── ScimUserEntityRepository.java │ │ │ └── ScimUserGroupEntityRepository.java │ │ ├── model │ │ │ ├── ScimAddressEntity.java │ │ │ ├── ScimEmailEntity.java │ │ │ ├── ScimGroupEntity.java │ │ │ ├── ScimImsEntity.java │ │ │ ├── ScimPhoneNumberEntity.java │ │ │ ├── ScimPhotoEntity.java │ │ │ ├── ScimResourceEntity.java │ │ │ ├── ScimUserEntity.java │ │ │ └── ScimUserGroupEntity.java │ │ ├── service │ │ │ ├── ScimGroupNotFoundException.java │ │ │ ├── ScimService.java │ │ │ └── ScimUserNotFoundException.java │ │ └── web │ │ │ └── ScimWebController.java │ │ ├── security │ │ ├── client │ │ │ ├── RegisteredClientAuthenticationService.java │ │ │ ├── RegisteredClientDetails.java │ │ │ └── RegisteredClientDetailsService.java │ │ └── user │ │ │ ├── EndUserDetails.java │ │ │ ├── EndUserDetailsService.java │ │ │ └── UserAuthenticationService.java │ │ ├── token │ │ ├── jwt │ │ │ └── JsonWebTokenService.java │ │ ├── opaque │ │ │ └── OpaqueTokenService.java │ │ └── store │ │ │ ├── TokenService.java │ │ │ ├── TokenServiceException.java │ │ │ ├── dao │ │ │ ├── JsonWebTokenRepository.java │ │ │ ├── OpaqueTokenRepository.java │ │ │ └── TokenRepository.java │ │ │ └── model │ │ │ ├── JsonWebToken.java │ │ │ ├── OpaqueToken.java │ │ │ └── Token.java │ │ └── web │ │ └── MainWebController.java └── resources │ ├── application.yml │ ├── messages.properties │ ├── messages_de.properties │ ├── messages_en.properties │ ├── static │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ └── js │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ └── bootstrap.min.js.map │ └── templates │ ├── admin.html │ ├── clientlist.html │ ├── consent.html │ ├── index.html │ └── userlist.html └── test ├── java └── com │ └── example │ └── authorizationserver │ ├── AuthorizationServerApplicationTests.java │ ├── annotation │ └── WebIntegrationTest.java │ ├── jwks │ └── JwksEndpointIntegrationTest.java │ ├── oauth │ ├── client │ │ └── api │ │ │ └── RegisteredClientApiControllerIntegrationTest.java │ └── endpoint │ │ ├── AuthorizationEndpointIntegrationTest.java │ │ ├── IntrospectionEndpointIntegrationTest.java │ │ ├── RevocationEndpointIntegrationTest.java │ │ └── TokenEndpointIntegrationTest.java │ ├── oidc │ └── endpoint │ │ ├── DiscoveryEndpointIntegrationTest.java │ │ └── UserInfoEndpointIntegrationTest.java │ ├── scim │ └── api │ │ ├── ScimGroupRestControllerIntegrationTest.java │ │ └── ScimUserRestControllerIntegrationTest.java │ └── token │ └── jwt │ └── JsonWebTokenServiceTest.java └── resources └── application-integration-test.yml /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '28 23 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'java', 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | - name: Setup Java JDK 51 | uses: actions/setup-java@v1 52 | with: 53 | java-version: 11 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v1 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 https://git.io/JvXDl 62 | 63 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 64 | # and modify them (or add more) to build your code if your project 65 | # uses a compiled language 66 | 67 | #- run: | 68 | # make bootstrap 69 | # make release 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v1 73 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | # test against latest update of each major Java version, as well as specific updates of LTS versions: 19 | java: [ 11, 11.0.3, 11.0.4, 11.0.5, 14 ] 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up JDK 23 | uses: actions/setup-java@v1 24 | with: 25 | java-version: ${{ matrix.java }} 26 | - name: Grant execute permission for gradlew 27 | run: chmod +x gradlew 28 | - name: Build with Gradle 29 | run: ./gradlew build 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: SecurityScan 4 | on: push 5 | jobs: 6 | security: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@master 10 | - name: Run Snyk to check for vulnerabilities 11 | uses: snyk/actions/gradle@master 12 | env: 13 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 14 | with: 15 | args: --severity-threshold=medium --fail-on=patchable 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 4 | ignore: 5 | SNYK-JAVA-COMH2DATABASE-31685: 6 | - '*': 7 | reason: no patch 8 | expires: 2020-06-30T00:00:00.000Z 9 | SNYK-JAVA-ORGYAML-537645: 10 | - '*': 11 | reason: already fixed 12 | expires: 2021-06-30T00:00:00.000Z 13 | patch: {} 14 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This project is not intended for production use. 4 | Vulnerabilities are fixed regularly but without any SLA or anything like this. 5 | 6 | ## Reporting a Vulnerability 7 | 8 | If you found a vulnerability just contact me via email or PN on Twitter. 9 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.6.2' 3 | id 'io.spring.dependency-management' version '1.0.11.RELEASE' 4 | id 'java' 5 | id 'com.adarshr.test-logger' version '2.1.1' 6 | id 'org.owasp.dependencycheck' version '6.5.1' 7 | } 8 | 9 | group = 'com.example' 10 | version = '1.0.0-SNAPSHOT' 11 | sourceCompatibility = '11' 12 | 13 | configurations { 14 | compileOnly { 15 | extendsFrom annotationProcessor 16 | } 17 | } 18 | 19 | repositories { 20 | mavenCentral() 21 | } 22 | 23 | dependencies { 24 | implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' 25 | implementation 'org.springframework.boot:spring-boot-starter-web' 26 | implementation 'org.springframework.boot:spring-boot-starter-security' 27 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 28 | implementation 'org.springframework.boot:spring-boot-starter-validation' 29 | implementation 'org.springdoc:springdoc-openapi-ui:1.6.2' 30 | implementation 'com.nimbusds:nimbus-jose-jwt:9.15.2' 31 | implementation 'org.apache.commons:commons-lang3' 32 | developmentOnly 'org.springframework.boot:spring-boot-devtools' 33 | runtimeOnly 'com.h2database:h2' 34 | runtimeOnly 'mysql:mysql-connector-java' 35 | runtimeOnly 'org.postgresql:postgresql' 36 | annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' 37 | testImplementation('org.springframework.boot:spring-boot-starter-test') 38 | testImplementation 'org.springframework.security:spring-security-test' 39 | testImplementation 'io.rest-assured:spring-mock-mvc' 40 | } 41 | 42 | test { 43 | useJUnitPlatform() 44 | } 45 | 46 | dependencyCheck { 47 | analyzedTypes = ['jar'] 48 | failBuildOnCVSS=8 49 | format='ALL' 50 | suppressionFile=file("$projectDir/dependency-check-suppressions.xml") 51 | analyzers { 52 | experimentalEnabled = false 53 | assemblyEnabled = false 54 | nuspecEnabled = false 55 | nodeEnabled = false 56 | nodeAuditEnabled = false 57 | nexusEnabled = false 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /dependency-check-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Only vulnerable when used in combination with Spring 5.0.5, we use 5.3+ 7 | ^pkg:maven/org\.springframework\.security/spring\-security\-.*@.*$ 8 | CVE-2018-1258 9 | 10 | 11 | Spring Security RSA uses a different version than Spring Security 12 | ^pkg:maven/org\.springframework\.security/spring\-security\-rsa@.*$ 13 | cpe:/a:pivotal:spring_security_oauth 14 | 15 | 16 | Spring Security RSA uses a different version than Spring Security 17 | ^pkg:maven/org\.springframework\.security/spring\-security\-rsa@.*$ 18 | cpe:/a:pivotal_software:spring_security 19 | 20 | 21 | Spring Security RSA uses a different version than Spring Security 22 | ^pkg:maven/org\.springframework\.security/spring\-security\-rsa@.*$ 23 | cpe:/a:vmware:springsource_spring_security 24 | 25 | 26 | 29 | a153c6f9744a3e9dd6feab5e210e1c9861362ec7 30 | cpe:/a:bouncycastle:legion-of-the-bouncy-castle 31 | 32 | 33 | 36 | a153c6f9744a3e9dd6feab5e210e1c9861362ec7 37 | cpe:/a:bouncycastle:legion-of-the-bouncy-castle-java-crytography-api 38 | 39 | 40 | 43 | ^pkg:maven/rubygems/jruby\-openssl@.*$ 44 | cpe:/a:jruby:jruby 45 | 46 | 47 | 50 | ^pkg:maven/rubygems/jruby\-openssl@.*$ 51 | cpe:/a:openssl:openssl 52 | 53 | 54 | 57 | ^pkg:maven/rubygems/jruby\-readline@.*$ 58 | cpe:/a:jruby:jruby 59 | 60 | 61 | 64 | ^pkg:maven/org\.jruby/dirgra@.*$ 65 | cpe:/a:jruby:jruby 66 | 67 | 68 | 71 | ^pkg:maven/org\.yaml/snakeyaml@.*$ 72 | cpe:/a:snakeyaml_project:snakeyaml 73 | 74 | 75 | 78 | ^pkg:maven/org\.yaml/snakeyaml@.*$ 79 | CVE-2017-18640 80 | 81 | 82 | 85 | ^pkg:javascript/jquery@.*$ 86 | CVE-2012-6708 87 | 88 | 89 | 92 | ^pkg:javascript/jquery@.*$ 93 | CVE-2015-9251 94 | 95 | 96 | 99 | ^pkg:javascript/jquery@.*$ 100 | CVE-2019-11358 101 | 102 | 103 | 106 | ^pkg:javascript/jquery@.*$ 107 | CVE-2020-11022 108 | 109 | 110 | 113 | ^pkg:javascript/jquery@.*$ 114 | CVE-2020-11023 115 | 116 | 117 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andifalk/authorizationserver/433da6335f5653e7fcb1b1ee82307d3c19a86faf/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'authorizationserver' 2 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/AuthorizationServerApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver; 2 | 3 | import com.example.authorizationserver.config.AuthorizationServerConfigurationProperties; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | 8 | @EnableConfigurationProperties(AuthorizationServerConfigurationProperties.class) 9 | @SpringBootApplication 10 | public class AuthorizationServerApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(AuthorizationServerApplication.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/admin/web/AdminWebController.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.admin.web; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class AdminWebController { 8 | 9 | @GetMapping("/admin") 10 | public String admin() { 11 | return "admin"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/config/AuditingConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 5 | 6 | @Configuration 7 | @EnableJpaAuditing 8 | public class AuditingConfiguration { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/config/AuthorizationServerConfigurationProperties.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | import java.net.URI; 6 | import java.time.Duration; 7 | 8 | @ConfigurationProperties(prefix = "auth-server") 9 | public class AuthorizationServerConfigurationProperties { 10 | 11 | private URI issuer; 12 | private AccessToken accessToken; 13 | private IdToken idToken; 14 | private RefreshToken refreshToken; 15 | 16 | public URI getIssuer() { 17 | return issuer; 18 | } 19 | 20 | public void setIssuer(URI issuer) { 21 | this.issuer = issuer; 22 | } 23 | 24 | public AccessToken getAccessToken() { 25 | return accessToken; 26 | } 27 | 28 | public void setAccessToken(AccessToken accessToken) { 29 | this.accessToken = accessToken; 30 | } 31 | 32 | public IdToken getIdToken() { 33 | return idToken; 34 | } 35 | 36 | public void setIdToken(IdToken idToken) { 37 | this.idToken = idToken; 38 | } 39 | 40 | public RefreshToken getRefreshToken() { 41 | return refreshToken; 42 | } 43 | 44 | public void setRefreshToken(RefreshToken refreshToken) { 45 | this.refreshToken = refreshToken; 46 | } 47 | 48 | public static abstract class Token { 49 | private Duration lifetime; 50 | 51 | public Duration getLifetime() { 52 | return lifetime; 53 | } 54 | 55 | public void setLifetime(Duration lifetime) { 56 | this.lifetime = lifetime; 57 | } 58 | } 59 | 60 | public enum TokenType { 61 | JWT, 62 | OPAQUE 63 | } 64 | 65 | public static class AccessToken extends Token { 66 | private TokenType defaultFormat; 67 | 68 | public TokenType getDefaultFormat() { 69 | return defaultFormat; 70 | } 71 | 72 | public void setDefaultFormat(TokenType defaultFormat) { 73 | this.defaultFormat = defaultFormat; 74 | } 75 | } 76 | 77 | public static class IdToken extends Token {} 78 | 79 | public static class RefreshToken extends Token { 80 | private Duration maxLifetime; 81 | 82 | public Duration getMaxLifetime() { 83 | return maxLifetime; 84 | } 85 | 86 | public void setMaxLifetime(Duration maxLifetime) { 87 | this.maxLifetime = maxLifetime; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/config/IdGeneratorConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.util.IdGenerator; 6 | import org.springframework.util.JdkIdGenerator; 7 | 8 | @Configuration 9 | public class IdGeneratorConfiguration { 10 | 11 | @Bean 12 | public IdGenerator idGenerator() { 13 | return new JdkIdGenerator(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/config/PasswordEncoderConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.crypto.factory.PasswordEncoderFactories; 6 | import org.springframework.security.crypto.password.PasswordEncoder; 7 | 8 | @Configuration 9 | public class PasswordEncoderConfiguration { 10 | 11 | @Bean 12 | public PasswordEncoder passwordEncoder() { 13 | return PasswordEncoderFactories.createDelegatingPasswordEncoder(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/jwks/JwksEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.jwks; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import org.springframework.web.bind.annotation.CrossOrigin; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import java.util.Map; 10 | 11 | @CrossOrigin(originPatterns = "*", allowCredentials = "true", allowedHeaders = "*") 12 | @RestController 13 | @RequestMapping(JwksEndpoint.ENDPOINT) 14 | public class JwksEndpoint { 15 | 16 | public static final String ENDPOINT = "/jwks"; 17 | 18 | private final JwtPki jwtPki; 19 | 20 | public JwksEndpoint(JwtPki jwtPki) { 21 | this.jwtPki = jwtPki; 22 | } 23 | 24 | @Operation( 25 | summary = "Retrieves the JSON web key set with public key(s) to validate tokens", 26 | tags = {"OpenID Connect Discovery"} 27 | ) 28 | @GetMapping 29 | public Map jwksEndpoint() { 30 | return jwtPki.getJwkSet().toJSONObject(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/jwks/JwtPki.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.jwks; 2 | 3 | import com.example.authorizationserver.config.AuthorizationServerConfigurationProperties; 4 | import com.nimbusds.jose.JOSEException; 5 | import com.nimbusds.jose.JWSSigner; 6 | import com.nimbusds.jose.JWSVerifier; 7 | import com.nimbusds.jose.crypto.RSASSASigner; 8 | import com.nimbusds.jose.crypto.RSASSAVerifier; 9 | import com.nimbusds.jose.jwk.JWKSet; 10 | import com.nimbusds.jose.jwk.RSAKey; 11 | import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; 12 | import org.springframework.stereotype.Component; 13 | 14 | import javax.annotation.PostConstruct; 15 | 16 | @Component 17 | public class JwtPki { 18 | 19 | private RSAKey publicKey; 20 | 21 | private JWKSet jwkSet; 22 | 23 | private JWSSigner signer; 24 | 25 | private JWSVerifier verifier; 26 | 27 | private final String issuer; 28 | 29 | public JwtPki(AuthorizationServerConfigurationProperties authorizationServerProperties) { 30 | this.issuer = authorizationServerProperties.getIssuer().toString(); 31 | } 32 | 33 | @PostConstruct 34 | public void initPki() throws JOSEException { 35 | RSAKey rsaJWK = new RSAKeyGenerator(2048).keyID("1").generate(); 36 | this.publicKey = rsaJWK.toPublicJWK(); 37 | this.signer = new RSASSASigner(rsaJWK); 38 | this.jwkSet = new JWKSet(this.publicKey); 39 | this.verifier = new RSASSAVerifier(this.publicKey); 40 | } 41 | 42 | public JWSSigner getSigner() { 43 | return signer; 44 | } 45 | 46 | public JWSVerifier getVerifier() { 47 | return verifier; 48 | } 49 | 50 | public RSAKey getPublicKey() { 51 | return publicKey; 52 | } 53 | 54 | public String getIssuer() { 55 | return issuer; 56 | } 57 | 58 | public JWKSet getJwkSet() { 59 | return jwkSet; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/client/RegisteredClientService.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.client; 2 | 3 | import com.example.authorizationserver.oauth.client.dao.RegisteredClientRepository; 4 | import com.example.authorizationserver.oauth.client.model.RegisteredClient; 5 | import org.springframework.security.access.prepost.PreAuthorize; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import java.util.List; 10 | import java.util.Optional; 11 | import java.util.UUID; 12 | 13 | @Transactional(readOnly = true) 14 | @Service 15 | public class RegisteredClientService { 16 | 17 | private final RegisteredClientRepository registeredClientRepository; 18 | 19 | public RegisteredClientService(RegisteredClientRepository registeredClientRepository) { 20 | this.registeredClientRepository = registeredClientRepository; 21 | } 22 | 23 | public Optional findOneByClientId(String clientId) { 24 | return registeredClientRepository.findOneByClientId(clientId); 25 | } 26 | 27 | @PreAuthorize("hasRole('ADMIN')") 28 | public List findAll() { 29 | return registeredClientRepository.findAll(); 30 | } 31 | 32 | @PreAuthorize("hasRole('ADMIN')") 33 | @Transactional 34 | public RegisteredClient create(RegisteredClient entity) { 35 | return registeredClientRepository.save(entity); 36 | } 37 | 38 | @PreAuthorize("hasRole('ADMIN')") 39 | public Optional findOneByIdentifier(UUID identifier) { 40 | return registeredClientRepository.findOneByIdentifier(identifier); 41 | } 42 | 43 | @PreAuthorize("hasRole('ADMIN')") 44 | @Transactional 45 | public void deleteOneByIdentifier(UUID identifier) { 46 | registeredClientRepository.deleteOneByIdentifier(identifier); 47 | } 48 | 49 | @Transactional 50 | public void deleteByClientId(String clientId) { 51 | registeredClientRepository.deleteByClientId(clientId); 52 | } 53 | 54 | @PreAuthorize("hasRole('ADMIN')") 55 | @Transactional 56 | public Optional update(UUID clientId, RegisteredClient registeredClientForUpdate) { 57 | return findOneByIdentifier(clientId).map(c -> { 58 | c.setAccessTokenFormat((registeredClientForUpdate.getAccessTokenFormat())); 59 | c.setClientSecret(registeredClientForUpdate.getClientSecret()); 60 | c.setConfidential(registeredClientForUpdate.isConfidential()); 61 | c.setCorsUris(registeredClientForUpdate.getCorsUris()); 62 | c.setGrantTypes(registeredClientForUpdate.getGrantTypes()); 63 | c.setRedirectUris(registeredClientForUpdate.getRedirectUris()); 64 | return Optional.of(registeredClientRepository.save(c)); 65 | }).orElse(Optional.empty()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/client/api/RegisteredClientApiController.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.client.api; 2 | 3 | import com.example.authorizationserver.oauth.client.RegisteredClientService; 4 | import com.example.authorizationserver.oauth.client.api.resource.ModifyRegisteredClientResource; 5 | import com.example.authorizationserver.oauth.client.api.resource.RegisteredClientResource; 6 | import com.example.authorizationserver.oauth.client.model.RegisteredClient; 7 | import io.swagger.v3.oas.annotations.Operation; 8 | import io.swagger.v3.oas.annotations.Parameter; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.DeleteMapping; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.PutMapping; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RestController; 18 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 19 | 20 | import javax.servlet.http.HttpServletRequest; 21 | import javax.validation.Valid; 22 | import java.net.URI; 23 | import java.util.List; 24 | import java.util.UUID; 25 | import java.util.stream.Collectors; 26 | 27 | @RestController 28 | @RequestMapping(RegisteredClientApiController.ENDPOINT) 29 | public class RegisteredClientApiController { 30 | 31 | public static final String ENDPOINT = "/api/clients"; 32 | 33 | private final RegisteredClientService registeredClientService; 34 | 35 | public RegisteredClientApiController(RegisteredClientService registeredClientService) { 36 | this.registeredClientService = registeredClientService; 37 | } 38 | 39 | @Operation( 40 | summary = "Retrieves list of registered clients", 41 | tags = {"Client Registration"}, 42 | parameters = { 43 | @Parameter(name = "groupId", description = "The identifier of the group", required = true, example = "4b2889df-3af6-4ad5-a889-4816c0ed8869"), 44 | @Parameter(name = "userId", description = "The identifier of the user", required = true, example = "4b2889df-3af6-4ad5-a889-4816c0ed8869") 45 | } 46 | ) 47 | @GetMapping 48 | public List clients() { 49 | return registeredClientService.findAll().stream() 50 | .map(RegisteredClientResource::new) 51 | .collect(Collectors.toList()); 52 | } 53 | 54 | @Operation( 55 | summary = "Retrieves a single registered client", 56 | tags = {"Client Registration"}, 57 | parameters = { 58 | @Parameter(name = "clientId", description = "The identifier of the client", required = true, example = "4b2889df-3af6-4ad5-a889-4816c0ed8869") 59 | } 60 | ) 61 | @GetMapping("/{clientId}") 62 | public ResponseEntity client(@PathVariable("clientId") UUID clientId) { 63 | return registeredClientService.findOneByIdentifier(clientId) 64 | .map(c -> ResponseEntity.ok(new RegisteredClientResource(c))) 65 | .orElse(ResponseEntity.notFound().build()); 66 | } 67 | 68 | @Operation( 69 | summary = "Registers a new client", 70 | tags = {"Client Registration"} 71 | ) 72 | @PostMapping 73 | public ResponseEntity registerNewClient( 74 | @RequestBody @Valid ModifyRegisteredClientResource modifyRegisteredClientResource, HttpServletRequest httpServletRequest) { 75 | 76 | RegisteredClient registeredClient = new RegisteredClient(modifyRegisteredClientResource); 77 | registeredClient = this.registeredClientService.create(registeredClient); 78 | 79 | URI uri = 80 | ServletUriComponentsBuilder.fromContextPath(httpServletRequest) 81 | .path("/api/clients/{clientId}") 82 | .buildAndExpand(registeredClient.getIdentifier()) 83 | .toUri(); 84 | return ResponseEntity.created(uri).body(new RegisteredClientResource(registeredClient)); 85 | } 86 | 87 | @Operation( 88 | summary = "Updates a single registered client", 89 | tags = {"Client Registration"}, 90 | parameters = { 91 | @Parameter(name = "clientId", description = "The identifier of the client", required = true, example = "4b2889df-3af6-4ad5-a889-4816c0ed8869") 92 | } 93 | ) 94 | @PutMapping("/{clientId}") 95 | public ResponseEntity update(@PathVariable("clientId") UUID clientId, 96 | @Valid @RequestBody ModifyRegisteredClientResource modifyRegisteredClientResource) { 97 | return registeredClientService 98 | .update(clientId, new RegisteredClient(modifyRegisteredClientResource)) 99 | .map(RegisteredClientResource::new) 100 | .map(ResponseEntity::ok) 101 | .orElse(ResponseEntity.notFound().build()); 102 | } 103 | 104 | @Operation( 105 | summary = "Deletes a registered client", 106 | tags = {"Client Registration"}, 107 | parameters = { 108 | @Parameter(name = "clientId", description = "The identifier of the client", required = true, example = "4b2889df-3af6-4ad5-a889-4816c0ed8869") 109 | } 110 | ) 111 | @DeleteMapping("/{clientId}") 112 | public ResponseEntity deleteUser(@PathVariable("clientId") UUID clientId) { 113 | registeredClientService.deleteOneByIdentifier(clientId); 114 | return ResponseEntity.noContent().build(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/client/api/resource/ModifyRegisteredClientResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.client.api.resource; 2 | 3 | import com.example.authorizationserver.oauth.client.model.RegisteredClient; 4 | 5 | import javax.validation.constraints.Size; 6 | 7 | public class ModifyRegisteredClientResource extends RegisteredClientResource { 8 | 9 | @Size(max = 100) 10 | private String clientSecret; 11 | 12 | public ModifyRegisteredClientResource() {} 13 | 14 | public ModifyRegisteredClientResource(RegisteredClient registeredClient) { 15 | super(registeredClient); 16 | this.clientSecret = registeredClient.getClientSecret(); 17 | } 18 | 19 | public String getClientSecret() { 20 | return clientSecret; 21 | } 22 | 23 | public void setClientSecret(String clientSecret) { 24 | this.clientSecret = clientSecret; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return "ModifyRegisteredClientResource{" + 30 | "clientSecret='" + clientSecret + '\'' + 31 | "} " + super.toString(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/client/api/resource/RegisteredClientResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.client.api.resource; 2 | 3 | import com.example.authorizationserver.oauth.client.model.AccessTokenFormat; 4 | import com.example.authorizationserver.oauth.client.model.RegisteredClient; 5 | import com.example.authorizationserver.oauth.common.GrantType; 6 | 7 | import javax.persistence.ElementCollection; 8 | import javax.persistence.EnumType; 9 | import javax.persistence.Enumerated; 10 | import javax.validation.constraints.NotBlank; 11 | import javax.validation.constraints.NotEmpty; 12 | import javax.validation.constraints.NotNull; 13 | import javax.validation.constraints.Size; 14 | import java.util.HashSet; 15 | import java.util.Set; 16 | import java.util.UUID; 17 | 18 | public class RegisteredClientResource { 19 | 20 | @NotNull private UUID identifier; 21 | 22 | @NotBlank 23 | @Size(max = 100) 24 | private String clientId; 25 | 26 | @NotNull private boolean confidential; 27 | 28 | @NotNull 29 | @Enumerated(EnumType.STRING) 30 | private AccessTokenFormat accessTokenFormat; 31 | 32 | /** Grants like 'client_credentials' or 'authorization_code' */ 33 | @NotEmpty 34 | @ElementCollection 35 | private Set grantTypes = new HashSet<>(); 36 | 37 | @NotEmpty private Set redirectUris = new HashSet<>(); 38 | 39 | @NotEmpty private Set corsUris = new HashSet<>(); 40 | 41 | public RegisteredClientResource() {} 42 | 43 | public RegisteredClientResource(RegisteredClient registeredClient) { 44 | this.identifier = registeredClient.getIdentifier(); 45 | this.accessTokenFormat = registeredClient.getAccessTokenFormat(); 46 | this.clientId = registeredClient.getClientId(); 47 | this.confidential = registeredClient.isConfidential(); 48 | this.corsUris = registeredClient.getCorsUris(); 49 | this.grantTypes = registeredClient.getGrantTypes(); 50 | this.redirectUris = registeredClient.getRedirectUris(); 51 | } 52 | 53 | public UUID getIdentifier() { 54 | return identifier; 55 | } 56 | 57 | public void setIdentifier(UUID identifier) { 58 | this.identifier = identifier; 59 | } 60 | 61 | public String getClientId() { 62 | return clientId; 63 | } 64 | 65 | public void setClientId(String clientId) { 66 | this.clientId = clientId; 67 | } 68 | 69 | public boolean isConfidential() { 70 | return confidential; 71 | } 72 | 73 | public void setConfidential(boolean confidential) { 74 | this.confidential = confidential; 75 | } 76 | 77 | public Set getGrantTypes() { 78 | return grantTypes; 79 | } 80 | 81 | public void setGrantTypes(Set grantTypes) { 82 | this.grantTypes = grantTypes; 83 | } 84 | 85 | public Set getRedirectUris() { 86 | return redirectUris; 87 | } 88 | 89 | public void setRedirectUris(Set redirectUris) { 90 | this.redirectUris = redirectUris; 91 | } 92 | 93 | public Set getCorsUris() { 94 | return corsUris; 95 | } 96 | 97 | public void setCorsUris(Set corsUris) { 98 | this.corsUris = corsUris; 99 | } 100 | 101 | public AccessTokenFormat getAccessTokenFormat() { 102 | return accessTokenFormat; 103 | } 104 | 105 | public void setAccessTokenFormat(AccessTokenFormat accessTokenFormat) { 106 | this.accessTokenFormat = accessTokenFormat; 107 | } 108 | 109 | @Override 110 | public String toString() { 111 | return "RegisteredClient{" 112 | + "clientId='" 113 | + clientId 114 | + '\'' 115 | + ", clientSecret='*****'" 116 | + ", confidential=" 117 | + confidential 118 | + ", accessTokenFormat=" 119 | + accessTokenFormat 120 | + ", grantTypes=" 121 | + grantTypes 122 | + ", redirectUris=" 123 | + redirectUris 124 | + ", corsUris=" 125 | + corsUris 126 | + '}'; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/client/dao/RegisteredClientRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.client.dao; 2 | 3 | import com.example.authorizationserver.oauth.client.model.RegisteredClient; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | import java.util.UUID; 8 | 9 | public interface RegisteredClientRepository extends JpaRepository { 10 | 11 | Optional findOneByClientId(String clientId); 12 | 13 | Optional findOneByIdentifier(UUID identifier); 14 | 15 | void deleteOneByIdentifier(UUID identifier); 16 | 17 | void deleteByClientId(String clientId); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/client/model/AccessTokenFormat.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.client.model; 2 | 3 | public enum AccessTokenFormat { 4 | OPAQUE, 5 | JWT 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/client/model/RegisteredClient.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.client.model; 2 | 3 | import com.example.authorizationserver.oauth.client.api.resource.ModifyRegisteredClientResource; 4 | import com.example.authorizationserver.oauth.common.GrantType; 5 | import org.springframework.data.jpa.domain.AbstractPersistable; 6 | 7 | import javax.persistence.Column; 8 | import javax.persistence.ElementCollection; 9 | import javax.persistence.Entity; 10 | import javax.persistence.EnumType; 11 | import javax.persistence.Enumerated; 12 | import javax.persistence.FetchType; 13 | import javax.validation.constraints.NotBlank; 14 | import javax.validation.constraints.NotEmpty; 15 | import javax.validation.constraints.NotNull; 16 | import javax.validation.constraints.Size; 17 | import java.net.URI; 18 | import java.util.HashSet; 19 | import java.util.Set; 20 | import java.util.UUID; 21 | 22 | @Entity 23 | public class RegisteredClient extends AbstractPersistable { 24 | 25 | public static final URI DEFAULT_REDIRECT_URI = URI.create("http://localhost:9090/demo-client/login/oauth2/code/demo"); 26 | 27 | /** Technical Identifier. */ 28 | @NotNull private UUID identifier; 29 | 30 | /** Unique identifier for client. */ 31 | @NotBlank 32 | @Size(max = 100) 33 | @Column(unique = true) 34 | private String clientId; 35 | 36 | /** Client secret, only needed for confidential clients. */ 37 | @Size(max = 100) 38 | private String clientSecret; 39 | 40 | /** 41 | * Confidential or Public client? Public Client: Requires PKCE but no clientSecret Confidential 42 | * Client: Requires clientSecret 43 | */ 44 | @NotNull private boolean confidential; 45 | 46 | /** Specifies format for access tokens: JWt or Opaque */ 47 | @NotNull 48 | @Enumerated(EnumType.STRING) 49 | private AccessTokenFormat accessTokenFormat; 50 | 51 | /** Grants like 'client_credentials' or 'authorization_code' */ 52 | @NotEmpty 53 | @ElementCollection(fetch = FetchType.EAGER) 54 | private Set grantTypes = new HashSet<>(); 55 | 56 | /** List of valid redirect URIs. */ 57 | @NotEmpty 58 | @ElementCollection(fetch = FetchType.EAGER) 59 | private Set redirectUris = new HashSet<>(); 60 | 61 | /** List of CORS origins allowed. */ 62 | @NotEmpty 63 | @ElementCollection(fetch = FetchType.EAGER) 64 | private Set corsUris = new HashSet<>(); 65 | 66 | public RegisteredClient() {} 67 | 68 | public RegisteredClient(ModifyRegisteredClientResource modifyRegisteredClientResource) { 69 | this(modifyRegisteredClientResource.getIdentifier(), modifyRegisteredClientResource.getClientId(), 70 | modifyRegisteredClientResource.getClientSecret(), modifyRegisteredClientResource.isConfidential(), 71 | modifyRegisteredClientResource.getAccessTokenFormat(), modifyRegisteredClientResource.getGrantTypes(), 72 | modifyRegisteredClientResource.getRedirectUris(), modifyRegisteredClientResource.getCorsUris()); 73 | } 74 | 75 | public RegisteredClient( 76 | UUID identifier, 77 | String clientId, 78 | String clientSecret, 79 | boolean confidential, 80 | AccessTokenFormat accessTokenFormat, 81 | Set grantTypes, 82 | Set redirectUris, 83 | Set corsUris) { 84 | this.identifier = identifier; 85 | this.accessTokenFormat = accessTokenFormat; 86 | this.clientId = clientId; 87 | this.clientSecret = clientSecret; 88 | this.confidential = confidential; 89 | this.grantTypes = grantTypes; 90 | this.corsUris = corsUris; 91 | this.redirectUris = redirectUris; 92 | } 93 | 94 | public UUID getIdentifier() { 95 | return identifier; 96 | } 97 | 98 | public void setIdentifier(UUID identifier) { 99 | this.identifier = identifier; 100 | } 101 | 102 | public String getClientId() { 103 | return clientId; 104 | } 105 | 106 | public void setClientId(String clientId) { 107 | this.clientId = clientId; 108 | } 109 | 110 | public String getClientSecret() { 111 | return clientSecret; 112 | } 113 | 114 | public void setClientSecret(String clientSecret) { 115 | this.clientSecret = clientSecret; 116 | } 117 | 118 | public boolean isConfidential() { 119 | return confidential; 120 | } 121 | 122 | public void setConfidential(boolean confidential) { 123 | this.confidential = confidential; 124 | } 125 | 126 | public Set getGrantTypes() { 127 | return grantTypes; 128 | } 129 | 130 | public void setGrantTypes(Set grantTypes) { 131 | this.grantTypes = grantTypes; 132 | } 133 | 134 | public Set getRedirectUris() { 135 | return redirectUris; 136 | } 137 | 138 | public void setRedirectUris(Set redirectUris) { 139 | this.redirectUris = redirectUris; 140 | } 141 | 142 | public Set getCorsUris() { 143 | return corsUris; 144 | } 145 | 146 | public void setCorsUris(Set corsUris) { 147 | this.corsUris = corsUris; 148 | } 149 | 150 | public AccessTokenFormat getAccessTokenFormat() { 151 | return accessTokenFormat; 152 | } 153 | 154 | public void setAccessTokenFormat(AccessTokenFormat accessTokenFormat) { 155 | this.accessTokenFormat = accessTokenFormat; 156 | } 157 | 158 | @Override 159 | public String toString() { 160 | return "RegisteredClient{" 161 | + "identifier='" 162 | + identifier 163 | + '\'' 164 | + ", clientId='" 165 | + clientId 166 | + ", clientSecret='*****'" 167 | + ", confidential=" 168 | + confidential 169 | + ", accessTokenFormat=" 170 | + accessTokenFormat 171 | + ", grantTypes=" 172 | + grantTypes 173 | + ", redirectUris=" 174 | + redirectUris 175 | + ", corsUris=" 176 | + corsUris 177 | + '}'; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/client/web/RegisteredClientController.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.client.web; 2 | 3 | import com.example.authorizationserver.oauth.client.RegisteredClientService; 4 | import com.example.authorizationserver.oauth.client.api.resource.RegisteredClientResource; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.ModelAttribute; 9 | 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | @Controller 14 | public class RegisteredClientController { 15 | 16 | private final RegisteredClientService registeredClientService; 17 | 18 | @Autowired 19 | public RegisteredClientController(RegisteredClientService registeredClientService) { 20 | this.registeredClientService = registeredClientService; 21 | } 22 | 23 | @ModelAttribute("allClients") 24 | public List populateUsers() { 25 | return this.registeredClientService.findAll().stream() 26 | .map(RegisteredClientResource::new) 27 | .collect(Collectors.toList()); 28 | } 29 | 30 | @GetMapping("/admin/clientlist") 31 | public String findAll() { 32 | return "clientlist"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/common/AuthenticationUtil.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.common; 2 | 3 | import org.springframework.security.authentication.BadCredentialsException; 4 | import org.springframework.util.StringUtils; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.Base64; 8 | 9 | public final class AuthenticationUtil { 10 | 11 | private static final String AUTHENTICATION_SCHEME_BASIC = "Basic"; 12 | private static final String AUTHENTICATION_SCHEME_BEARER = "Bearer"; 13 | 14 | private AuthenticationUtil() {} 15 | 16 | public static ClientCredentials fromBasicAuthHeader(String header) { 17 | if (header == null) { 18 | return null; 19 | } 20 | 21 | header = header.trim(); 22 | if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) { 23 | return null; 24 | } 25 | 26 | byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8); 27 | byte[] decoded; 28 | try { 29 | decoded = Base64.getDecoder().decode(base64Token); 30 | } catch (IllegalArgumentException e) { 31 | throw new BadCredentialsException("Failed to decode basic authentication token"); 32 | } 33 | 34 | String token = new String(decoded, StandardCharsets.UTF_8); 35 | 36 | int delim = token.indexOf(":"); 37 | 38 | if (delim == -1) { 39 | throw new BadCredentialsException("Invalid basic authentication token"); 40 | } 41 | return new ClientCredentials(token.substring(0, delim), token.substring(delim + 1)); 42 | } 43 | 44 | public static String fromBearerAuthHeader(String header) { 45 | if (header == null) { 46 | return null; 47 | } 48 | 49 | header = header.trim(); 50 | if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BEARER)) { 51 | return null; 52 | } 53 | 54 | return header.substring(7); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/common/ClientCredentials.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.common; 2 | 3 | public class ClientCredentials { 4 | 5 | private final String clientId; 6 | private final String clientSecret; 7 | 8 | public ClientCredentials(String clientId, String clientSecret) { 9 | this.clientId = clientId; 10 | this.clientSecret = clientSecret; 11 | } 12 | 13 | public String getClientId() { 14 | return clientId; 15 | } 16 | 17 | public String getClientSecret() { 18 | return clientSecret; 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | return "ClientCredentials{" + "clientId='" + clientId + '\'' + ", clientSecret='*****'" + '}'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/common/GrantType.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.common; 2 | 3 | public enum GrantType { 4 | AUTHORIZATION_CODE("authorization_code"), 5 | PASSWORD("password"), 6 | CLIENT_CREDENTIALS("client_credentials"), 7 | REFRESH_TOKEN("refresh_token"), 8 | TOKEN_EXCHANGE("urn:ietf:params:oauth:grant-type:token-exchange"); 9 | 10 | private final String grant; 11 | 12 | GrantType(String grant) { 13 | this.grant = grant; 14 | } 15 | 16 | public String getGrant() { 17 | return grant; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/common/ResponseType.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.common; 2 | 3 | public enum ResponseType { 4 | CODE, 5 | ID_TOKEN, 6 | TOKEN 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/endpoint/introspection/resource/IntrospectionRequest.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.endpoint.introspection.resource; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | 5 | public class IntrospectionRequest { 6 | 7 | public IntrospectionRequest() {} 8 | 9 | public IntrospectionRequest(@NotBlank String token, String token_type_hint) { 10 | this.token = token; 11 | this.token_type_hint = token_type_hint; 12 | } 13 | 14 | /** 15 | * REQUIRED. The string value of the token. For access tokens, this is the "access_token" value 16 | * returned from the token endpoint defined in OAuth 2.0 [RFC6749], Section 5.1. For refresh 17 | * tokens, this is the "refresh_token" value returned from the token endpoint as defined in OAuth 18 | * 2.0 [RFC6749], Section 5.1. Other token types are outside the scope of this specification. 19 | */ 20 | @NotBlank private String token; 21 | 22 | /** 23 | * OPTIONAL. A hint about the type of the token submitted for introspection. The protected 24 | * resource MAY pass this parameter to help the authorization server optimize the token lookup. If 25 | * the server is unable to locate the token using the given hint, it MUST extend its search across 26 | * all of its supported token types. An authorization server MAY ignore this parameter, 27 | * particularly if it is able to detect the token type automatically. Values for this field are 28 | * defined in the "OAuth Token Type Hints" registry defined in OAuth Token Revocation [RFC7009]. 29 | */ 30 | private String token_type_hint; 31 | 32 | public String getToken() { 33 | return token; 34 | } 35 | 36 | public void setToken(String token) { 37 | this.token = token; 38 | } 39 | 40 | public String getToken_type_hint() { 41 | return token_type_hint; 42 | } 43 | 44 | public void setToken_type_hint(String token_type_hint) { 45 | this.token_type_hint = token_type_hint; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return "IntrospectionRequest{" 51 | + "token='" 52 | + token 53 | + '\'' 54 | + ", token_type_hint='" 55 | + token_type_hint 56 | + '\'' 57 | + '}'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/endpoint/revocation/RevocationEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.endpoint.revocation; 2 | 3 | import com.example.authorizationserver.oauth.common.AuthenticationUtil; 4 | import com.example.authorizationserver.oauth.common.ClientCredentials; 5 | import com.example.authorizationserver.oauth.endpoint.revocation.resource.RevocationRequest; 6 | import com.example.authorizationserver.oauth.endpoint.revocation.resource.RevocationResponse; 7 | import com.example.authorizationserver.security.client.RegisteredClientAuthenticationService; 8 | import com.example.authorizationserver.token.store.TokenService; 9 | import com.example.authorizationserver.token.store.model.JsonWebToken; 10 | import com.example.authorizationserver.token.store.model.OpaqueToken; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.security.authentication.BadCredentialsException; 15 | import org.springframework.security.core.AuthenticationException; 16 | import org.springframework.validation.BindingResult; 17 | import org.springframework.web.bind.annotation.CrossOrigin; 18 | import org.springframework.web.bind.annotation.ModelAttribute; 19 | import org.springframework.web.bind.annotation.PostMapping; 20 | import org.springframework.web.bind.annotation.RequestHeader; 21 | import org.springframework.web.bind.annotation.RequestMapping; 22 | import org.springframework.web.bind.annotation.RestController; 23 | 24 | import static org.springframework.http.HttpStatus.UNAUTHORIZED; 25 | 26 | /** OAuth 2.0 Token Revocation as specified in https://tools.ietf.org/html/rfc7009 */ 27 | @CrossOrigin(originPatterns = "*", allowCredentials = "true", allowedHeaders = "*") 28 | @RestController 29 | @RequestMapping(RevocationEndpoint.ENDPOINT) 30 | public class RevocationEndpoint { 31 | 32 | public static final String ENDPOINT = "/revoke"; 33 | 34 | private static final Logger LOG = LoggerFactory.getLogger(RevocationEndpoint.class); 35 | 36 | private final TokenService tokenService; 37 | private final RegisteredClientAuthenticationService registeredClientAuthenticationService; 38 | 39 | public RevocationEndpoint( 40 | TokenService tokenService, 41 | RegisteredClientAuthenticationService registeredClientAuthenticationService) { 42 | this.tokenService = tokenService; 43 | this.registeredClientAuthenticationService = registeredClientAuthenticationService; 44 | } 45 | 46 | /** 47 | * Revocation Request. The client constructs the request by including the following parameters 48 | * using the "application/x-www-form-urlencoded" format in the HTTP request entity-body. 49 | * 50 | * @param authorizationHeader the authorization header for authenticating the client 51 | * @param revocationRequest Revocation Request 52 | * @param result Validation result of request parameters 53 | * @return the Revocation Response as specified in rfc7009 54 | */ 55 | @PostMapping 56 | public ResponseEntity revoke( 57 | @RequestHeader(name = "Authorization", required = false) String authorizationHeader, 58 | @ModelAttribute("revocation_request") RevocationRequest revocationRequest, 59 | BindingResult result) { 60 | 61 | ClientCredentials clientCredentials; 62 | 63 | try { 64 | 65 | clientCredentials = AuthenticationUtil.fromBasicAuthHeader(authorizationHeader); 66 | if (clientCredentials != null) { 67 | try { 68 | registeredClientAuthenticationService.authenticate( 69 | clientCredentials.getClientId(), clientCredentials.getClientSecret()); 70 | } catch (AuthenticationException ex) { 71 | return ResponseEntity.status(UNAUTHORIZED).header("WWW-Authenticate", "Basic") 72 | .body(new RevocationResponse(null, "invalid_client")); 73 | } 74 | } 75 | 76 | /* 77 | A hint about the type of the token 78 | submitted for revocation. Clients MAY pass this parameter in 79 | order to help the authorization server to optimize the token 80 | lookup. If the server is unable to locate the token using 81 | the given hint, it MUST extend its search across all of its 82 | supported token types. An authorization server MAY ignore 83 | this parameter, particularly if it is able to detect the 84 | token type automatically. 85 | */ 86 | 87 | LOG.debug("Revocation request [{}]", revocationRequest); 88 | 89 | OpaqueToken opaqueWebToken = tokenService.findOpaqueToken(revocationRequest.getToken()); 90 | if (opaqueWebToken != null) { 91 | tokenService.remove(opaqueWebToken); 92 | LOG.info("[{}] token (Opaque) has been revoked", opaqueWebToken.isRefreshToken() ? "Refresh" : "Access"); 93 | } else { 94 | JsonWebToken jsonWebToken = tokenService.findJsonWebAccessToken(revocationRequest.getToken()); 95 | if (jsonWebToken != null) { 96 | tokenService.remove(jsonWebToken); 97 | LOG.info("Access token (JWT) has been revoked"); 98 | } else { 99 | return ResponseEntity.badRequest().body(new RevocationResponse(null, "invalid_request")); 100 | } 101 | } 102 | return ResponseEntity.ok(new RevocationResponse("ok", null)); 103 | } catch (BadCredentialsException ex) { 104 | return reportInvalidClientError(); 105 | } 106 | } 107 | 108 | private ResponseEntity reportInvalidClientError() { 109 | return ResponseEntity.status(UNAUTHORIZED) 110 | .header("WWW-Authenticate", "Basic") 111 | .body(new RevocationResponse(null, "invalid_client")); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/endpoint/revocation/resource/RevocationRequest.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.endpoint.revocation.resource; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Pattern; 5 | 6 | /** 7 | * Revocation request as defined in https://www.rfc-editor.org/rfc/rfc7009.html. 8 | */ 9 | public class RevocationRequest { 10 | 11 | /** 12 | * REQUIRED. The token that the client wants to get revoked. 13 | */ 14 | @NotBlank private String token; 15 | 16 | /** 17 | * OPTIONAL. A hint about the type of the token submitted for revocation. Clients MAY pass this 18 | * parameter in order to help the authorization server to optimize the token lookup. If the server 19 | * is unable to locate the token using the given hint, it MUST extend its search across all of its 20 | * supported token types. An authorization server MAY ignore this parameter, particularly if it is 21 | * able to detect the token type automatically. This specification defines two such values: 22 | * 23 | *
    24 | *
  • access_token: An access token as defined in RFC6749, Section 1.4
  • 25 | *
  • refresh_token: A refresh token as defined in [RFC6749], Section 1.5
  • 26 | *
27 | */ 28 | @Pattern(regexp = "access_token|refresh_token") 29 | private String token_type_hint; 30 | 31 | public String getToken() { 32 | return token; 33 | } 34 | 35 | public void setToken(String token) { 36 | this.token = token; 37 | } 38 | 39 | public String getToken_type_hint() { 40 | return token_type_hint; 41 | } 42 | 43 | public void setToken_type_hint(String token_type_hint) { 44 | this.token_type_hint = token_type_hint; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return "IntrospectionRequest{" 50 | + "token='" 51 | + token 52 | + '\'' 53 | + ", token_type_hint='" 54 | + token_type_hint 55 | + '\'' 56 | + '}'; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/endpoint/revocation/resource/RevocationResponse.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.endpoint.revocation.resource; 2 | 3 | /** 4 | * Revocation response as defined in https://www.rfc-editor.org/rfc/rfc7009.html. 5 | */ 6 | public class RevocationResponse { 7 | 8 | private String status; 9 | private String error; 10 | 11 | public RevocationResponse(String status, String error) { 12 | this.status = status; 13 | this.error = error; 14 | } 15 | 16 | public String getError() { 17 | return error; 18 | } 19 | 20 | public void setError(String error) { 21 | this.error = error; 22 | } 23 | 24 | public String getStatus() { 25 | return status; 26 | } 27 | 28 | public void setStatus(String status) { 29 | this.status = status; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "RevocationResponse{" 35 | + "status='" 36 | + status 37 | + '\'' 38 | + ", error='" 39 | + error 40 | + '\'' 41 | + '}'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/endpoint/token/ClientCredentialsTokenEndpointService.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.endpoint.token; 2 | 3 | import com.example.authorizationserver.config.AuthorizationServerConfigurationProperties; 4 | import com.example.authorizationserver.oauth.client.model.AccessTokenFormat; 5 | import com.example.authorizationserver.oauth.client.model.RegisteredClient; 6 | import com.example.authorizationserver.oauth.common.ClientCredentials; 7 | import com.example.authorizationserver.oauth.common.GrantType; 8 | import com.example.authorizationserver.oauth.endpoint.token.resource.TokenRequest; 9 | import com.example.authorizationserver.oauth.endpoint.token.resource.TokenResponse; 10 | import com.example.authorizationserver.security.client.RegisteredClientAuthenticationService; 11 | import com.example.authorizationserver.token.store.TokenService; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.security.core.AuthenticationException; 17 | import org.springframework.stereotype.Service; 18 | 19 | import java.time.Duration; 20 | import java.util.Arrays; 21 | import java.util.HashSet; 22 | import java.util.Set; 23 | 24 | import static com.example.authorizationserver.oauth.endpoint.token.resource.TokenResponse.BEARER_TOKEN_TYPE; 25 | 26 | @Service 27 | public class ClientCredentialsTokenEndpointService { 28 | private static final Logger LOG = 29 | LoggerFactory.getLogger(ClientCredentialsTokenEndpointService.class); 30 | 31 | private final TokenService tokenService; 32 | private final AuthorizationServerConfigurationProperties authorizationServerProperties; 33 | private final RegisteredClientAuthenticationService registeredClientAuthenticationService; 34 | 35 | public ClientCredentialsTokenEndpointService( 36 | TokenService tokenService, 37 | AuthorizationServerConfigurationProperties authorizationServerProperties, 38 | RegisteredClientAuthenticationService registeredClientAuthenticationService) { 39 | this.tokenService = tokenService; 40 | this.authorizationServerProperties = authorizationServerProperties; 41 | this.registeredClientAuthenticationService = registeredClientAuthenticationService; 42 | } 43 | 44 | /* ------------------- 45 | Access Token Request 46 | 47 | The client makes a request to the token endpoint by adding the 48 | following parameters using the "application/x-www-form-urlencoded" 49 | format per Appendix B with a character encoding of UTF-8 in the HTTP 50 | request entity-body: 51 | 52 | grant_type 53 | REQUIRED. Value MUST be set to "client_credentials". 54 | 55 | scope 56 | OPTIONAL. The scope of the access request as described by 57 | Section 3.3. 58 | 59 | The client MUST authenticate with the authorization server 60 | */ 61 | public ResponseEntity getTokenResponseForClientCredentials( 62 | String authorizationHeader, TokenRequest tokenRequest) { 63 | 64 | LOG.debug("Exchange token for 'client credentials' with [{}]", tokenRequest); 65 | 66 | ClientCredentials clientCredentials = 67 | TokenEndpointHelper.retrieveClientCredentials(authorizationHeader, tokenRequest); 68 | 69 | if (clientCredentials == null) { 70 | return TokenEndpointHelper.reportInvalidClientError(); 71 | } 72 | 73 | Duration accessTokenLifetime = authorizationServerProperties.getAccessToken().getLifetime(); 74 | Duration refreshTokenLifetime = authorizationServerProperties.getRefreshToken().getLifetime(); 75 | 76 | RegisteredClient registeredClient; 77 | 78 | try { 79 | registeredClient = 80 | registeredClientAuthenticationService.authenticate( 81 | clientCredentials.getClientId(), clientCredentials.getClientSecret()); 82 | 83 | } catch (AuthenticationException ex) { 84 | return TokenEndpointHelper.reportInvalidClientError(); 85 | } 86 | 87 | if (registeredClient.getGrantTypes().contains(GrantType.CLIENT_CREDENTIALS)) { 88 | 89 | Set scopes = new HashSet<>(); 90 | if (StringUtils.isNotBlank(tokenRequest.getScope())) { 91 | scopes = new HashSet<>(Arrays.asList(tokenRequest.getScope().split(" "))); 92 | } 93 | 94 | LOG.info( 95 | "Creating token response for client credentials for client [{}]", 96 | tokenRequest.getClient_id()); 97 | return ResponseEntity.ok( 98 | new TokenResponse( 99 | AccessTokenFormat.JWT.equals(registeredClient.getAccessTokenFormat()) 100 | ? tokenService 101 | .createAnonymousJwtAccessToken( 102 | clientCredentials.getClientId(), scopes, accessTokenLifetime) 103 | .getValue() 104 | : tokenService 105 | .createAnonymousOpaqueAccessToken( 106 | clientCredentials.getClientId(), scopes, accessTokenLifetime) 107 | .getValue(), 108 | tokenService 109 | .createAnonymousRefreshToken( 110 | clientCredentials.getClientId(), scopes, refreshTokenLifetime) 111 | .getValue(), 112 | accessTokenLifetime.toSeconds(), 113 | null, 114 | BEARER_TOKEN_TYPE)); 115 | } else { 116 | return TokenEndpointHelper.reportUnauthorizedClientError(); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/endpoint/token/TokenEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.endpoint.token; 2 | 3 | import com.example.authorizationserver.oauth.common.GrantType; 4 | import com.example.authorizationserver.oauth.endpoint.token.resource.TokenRequest; 5 | import com.example.authorizationserver.oauth.endpoint.token.resource.TokenResponse; 6 | import com.nimbusds.jose.JOSEException; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.security.authentication.BadCredentialsException; 12 | import org.springframework.web.bind.MissingServletRequestParameterException; 13 | import org.springframework.web.bind.annotation.CrossOrigin; 14 | import org.springframework.web.bind.annotation.ExceptionHandler; 15 | import org.springframework.web.bind.annotation.ModelAttribute; 16 | import org.springframework.web.bind.annotation.PostMapping; 17 | import org.springframework.web.bind.annotation.RequestHeader; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.RestController; 20 | 21 | @CrossOrigin(originPatterns = "*", allowCredentials = "true", allowedHeaders = "*") 22 | @RequestMapping(TokenEndpoint.ENDPOINT) 23 | @RestController 24 | public class TokenEndpoint { 25 | public static final String ENDPOINT = "/token"; 26 | private static final Logger LOG = LoggerFactory.getLogger(TokenEndpoint.class); 27 | 28 | private final ClientCredentialsTokenEndpointService clientCredentialsTokenEndpointService; 29 | private final PasswordTokenEndpointService passwordTokenEndpointService; 30 | private final RefreshTokenEndpointService refreshTokenEndpointService; 31 | private final AuthorizationCodeTokenEndpointService authorizationCodeTokenEndpointService; 32 | 33 | public TokenEndpoint( 34 | ClientCredentialsTokenEndpointService clientCredentialsTokenEndpointService, 35 | PasswordTokenEndpointService passwordTokenEndpointService, 36 | RefreshTokenEndpointService refreshTokenEndpointService, 37 | AuthorizationCodeTokenEndpointService authorizationCodeTokenEndpointService) { 38 | this.clientCredentialsTokenEndpointService = clientCredentialsTokenEndpointService; 39 | this.passwordTokenEndpointService = passwordTokenEndpointService; 40 | this.refreshTokenEndpointService = refreshTokenEndpointService; 41 | this.authorizationCodeTokenEndpointService = authorizationCodeTokenEndpointService; 42 | } 43 | 44 | @PostMapping 45 | public ResponseEntity getToken( 46 | @RequestHeader(name = "Authorization", required = false) String authorizationHeader, 47 | @ModelAttribute("token_request") TokenRequest tokenRequest) { 48 | 49 | LOG.debug("Exchange token with grant type [{}]", tokenRequest.getGrant_type()); 50 | 51 | if (tokenRequest.getGrant_type().equalsIgnoreCase(GrantType.CLIENT_CREDENTIALS.getGrant())) { 52 | return clientCredentialsTokenEndpointService.getTokenResponseForClientCredentials( 53 | authorizationHeader, tokenRequest); 54 | } else if (tokenRequest.getGrant_type().equalsIgnoreCase(GrantType.PASSWORD.getGrant())) { 55 | return passwordTokenEndpointService.getTokenResponseForPassword( 56 | authorizationHeader, tokenRequest); 57 | } else if (tokenRequest 58 | .getGrant_type() 59 | .equalsIgnoreCase(GrantType.AUTHORIZATION_CODE.getGrant())) { 60 | return authorizationCodeTokenEndpointService.getTokenResponseForAuthorizationCode( 61 | authorizationHeader, tokenRequest); 62 | } else if (tokenRequest.getGrant_type().equalsIgnoreCase(GrantType.REFRESH_TOKEN.getGrant())) { 63 | return refreshTokenEndpointService.getTokenResponseForRefreshToken( 64 | authorizationHeader, tokenRequest); 65 | } else if (tokenRequest.getGrant_type().equalsIgnoreCase(GrantType.TOKEN_EXCHANGE.getGrant())) { 66 | LOG.warn("Requested grant type for 'Token Exchange' is not yet supported"); 67 | return ResponseEntity.badRequest().body(new TokenResponse("unsupported_grant_type")); 68 | } else { 69 | LOG.warn("Requested grant type [{}] is unsupported", tokenRequest.getGrant_type()); 70 | return ResponseEntity.badRequest().body(new TokenResponse("unsupported_grant_type")); 71 | } 72 | } 73 | 74 | @ExceptionHandler(MissingServletRequestParameterException.class) 75 | public ResponseEntity handle(MissingServletRequestParameterException ex) { 76 | return ResponseEntity.badRequest().body(ex.getMessage()); 77 | } 78 | 79 | @ExceptionHandler(BadCredentialsException.class) 80 | public ResponseEntity handle(BadCredentialsException ex) { 81 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getMessage()); 82 | } 83 | 84 | @ExceptionHandler(JOSEException.class) 85 | public ResponseEntity handle(JOSEException ex) { 86 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/endpoint/token/TokenEndpointHelper.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.endpoint.token; 2 | 3 | import com.example.authorizationserver.oauth.common.AuthenticationUtil; 4 | import com.example.authorizationserver.oauth.common.ClientCredentials; 5 | import com.example.authorizationserver.oauth.endpoint.token.resource.TokenRequest; 6 | import com.example.authorizationserver.oauth.endpoint.token.resource.TokenResponse; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | 11 | public final class TokenEndpointHelper { 12 | 13 | private TokenEndpointHelper() {} 14 | 15 | static ClientCredentials retrieveClientCredentials( 16 | String authorizationHeader, TokenRequest tokenRequest) { 17 | ClientCredentials clientCredentials = null; 18 | if (authorizationHeader != null) { 19 | clientCredentials = AuthenticationUtil.fromBasicAuthHeader(authorizationHeader); 20 | } else if (StringUtils.isNotBlank(tokenRequest.getClient_id())) { 21 | clientCredentials = 22 | new ClientCredentials(tokenRequest.getClient_id(), tokenRequest.getClient_secret()); 23 | } 24 | return clientCredentials; 25 | } 26 | 27 | static ResponseEntity reportUnauthorizedClientError() { 28 | return ResponseEntity.badRequest().body(new TokenResponse("unauthorized_client")); 29 | } 30 | 31 | static ResponseEntity reportInvalidClientError() { 32 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED) 33 | .header("WWW-Authenticate", "Basic") 34 | .body(new TokenResponse("invalid_client")); 35 | } 36 | 37 | static ResponseEntity reportInvalidGrantError() { 38 | return ResponseEntity.badRequest().body(new TokenResponse("invalid_grant")); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/endpoint/token/resource/TokenRequest.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.endpoint.token.resource; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.NotNull; 5 | import java.net.URI; 6 | 7 | /** 8 | * Token Request as specified by: 9 | * 10 | *

OAuth 2.0 (https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.3) OpenID Connect 1.0 11 | * (https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest) 12 | */ 13 | public class TokenRequest { 14 | 15 | /** 16 | * Authorization Grant Type. REQUIRED One of {@link 17 | * com.example.authorizationserver.oauth.common.GrantType} 18 | */ 19 | @NotBlank private final String grant_type; 20 | 21 | /** 22 | * Authorization code. REQUIRED if grant type is {@link 23 | * com.example.authorizationserver.oauth.common.GrantType#AUTHORIZATION_CODE} 24 | */ 25 | private final String code; 26 | 27 | /** 28 | * Redirect URI. REQUIRED if grant type is {@link 29 | * com.example.authorizationserver.oauth.common.GrantType#AUTHORIZATION_CODE} 30 | */ 31 | private final URI redirect_uri; 32 | 33 | /** Client Id. REQUIRED if not given by authorization header */ 34 | private final String client_id; 35 | 36 | /** 37 | * Client Secret. REQUIRED for confidential client if not given by authorization header Applicable 38 | * for grant type is {@link 39 | * com.example.authorizationserver.oauth.common.GrantType#AUTHORIZATION_CODE} or {@link 40 | * com.example.authorizationserver.oauth.common.GrantType#CLIENT_CREDENTIALS} 41 | */ 42 | private final String client_secret; 43 | 44 | /** Unhashed Code Verifier. REQUIRED for PKCE. */ 45 | private final String code_verifier; 46 | 47 | /** 48 | * The resource owner username REQUIRED if grant type is {@link 49 | * com.example.authorizationserver.oauth.common.GrantType#PASSWORD} 50 | */ 51 | private final String username; 52 | 53 | /** 54 | * The resource owner password. REQUIRED if grant type is {@link 55 | * com.example.authorizationserver.oauth.common.GrantType#PASSWORD} 56 | */ 57 | private final String password; 58 | 59 | /** 60 | * The refresh token issued to the client. REQUIRED if grant type is {@link 61 | * com.example.authorizationserver.oauth.common.GrantType#REFRESH_TOKEN} 62 | */ 63 | private final String refresh_token; 64 | 65 | /** 66 | * The scope of the access request. OPTIONAL if grant type is {@link 67 | * com.example.authorizationserver.oauth.common.GrantType#CLIENT_CREDENTIALS} 68 | */ 69 | private final String scope; 70 | 71 | public TokenRequest( 72 | @NotBlank String grant_type, 73 | @NotBlank String code, 74 | @NotNull URI redirect_uri, 75 | String client_id, 76 | String client_secret, 77 | String code_verifier, 78 | String username, 79 | String password, 80 | String refresh_token, 81 | String scope) { 82 | this.grant_type = grant_type; 83 | this.code = code; 84 | this.redirect_uri = redirect_uri; 85 | this.client_id = client_id; 86 | this.client_secret = client_secret; 87 | this.code_verifier = code_verifier; 88 | this.username = username; 89 | this.password = password; 90 | this.refresh_token = refresh_token; 91 | this.scope = scope; 92 | } 93 | 94 | public String getGrant_type() { 95 | return grant_type; 96 | } 97 | 98 | public String getCode() { 99 | return code; 100 | } 101 | 102 | public URI getRedirect_uri() { 103 | return redirect_uri; 104 | } 105 | 106 | public String getClient_id() { 107 | return client_id; 108 | } 109 | 110 | public String getClient_secret() { 111 | return client_secret; 112 | } 113 | 114 | public String getCode_verifier() { 115 | return code_verifier; 116 | } 117 | 118 | public String getUsername() { 119 | return username; 120 | } 121 | 122 | public String getPassword() { 123 | return password; 124 | } 125 | 126 | public String getRefresh_token() { 127 | return refresh_token; 128 | } 129 | 130 | public String getScope() { 131 | return scope; 132 | } 133 | 134 | @Override 135 | public String toString() { 136 | return "TokenRequest{" 137 | + "grant_type='" 138 | + grant_type 139 | + '\'' 140 | + ", code='" 141 | + code 142 | + '\'' 143 | + ", redirect_uri=" 144 | + redirect_uri 145 | + ", client_id='" 146 | + client_id 147 | + '\'' 148 | + ", client_secret='*****'" 149 | + ", code_verifier='" 150 | + code_verifier 151 | + '\'' 152 | + ", refresh_token='" 153 | + refresh_token 154 | + '\'' 155 | + ", scope='" 156 | + scope 157 | + '\'' 158 | + ", username='" 159 | + username 160 | + '\'' 161 | + ", password='*****'" 162 | + '}'; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/endpoint/token/resource/TokenResponse.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.endpoint.token.resource; 2 | 3 | /** 4 | * Token Response as specified by: 5 | * 6 | *

OAuth 2.0 (https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.3) OpenID Connect 1.0 7 | * (https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest) 8 | */ 9 | public class TokenResponse { 10 | 11 | public static final String BEARER_TOKEN_TYPE = "Bearer"; 12 | 13 | private String access_token; 14 | private String token_type; 15 | private String refresh_token; 16 | private long expires_in; 17 | private String id_token; 18 | private String error; 19 | 20 | public TokenResponse( 21 | String access_token, 22 | String refresh_token, 23 | long expires_in, 24 | String id_token, 25 | String token_type) { 26 | this.access_token = access_token; 27 | this.refresh_token = refresh_token; 28 | this.expires_in = expires_in; 29 | this.id_token = id_token; 30 | this.token_type = token_type; 31 | } 32 | 33 | public TokenResponse(String error) { 34 | this.error = error; 35 | } 36 | 37 | public String getAccess_token() { 38 | return access_token; 39 | } 40 | 41 | public void setAccess_token(String access_token) { 42 | this.access_token = access_token; 43 | } 44 | 45 | public String getToken_type() { 46 | return token_type; 47 | } 48 | 49 | public void setToken_type(String token_type) { 50 | this.token_type = token_type; 51 | } 52 | 53 | public String getRefresh_token() { 54 | return refresh_token; 55 | } 56 | 57 | public void setRefresh_token(String refresh_token) { 58 | this.refresh_token = refresh_token; 59 | } 60 | 61 | public long getExpires_in() { 62 | return expires_in; 63 | } 64 | 65 | public void setExpires_in(long expires_in) { 66 | this.expires_in = expires_in; 67 | } 68 | 69 | public String getId_token() { 70 | return id_token; 71 | } 72 | 73 | public void setId_token(String id_token) { 74 | this.id_token = id_token; 75 | } 76 | 77 | public String getError() { 78 | return error; 79 | } 80 | 81 | public void setError(String error) { 82 | this.error = error; 83 | } 84 | 85 | @Override 86 | public String toString() { 87 | return "TokenResponse{" 88 | + "access_token='" 89 | + access_token 90 | + '\'' 91 | + ", token_type='" 92 | + token_type 93 | + '\'' 94 | + ", refresh_token='" 95 | + refresh_token 96 | + '\'' 97 | + ", expires_in=" 98 | + expires_in 99 | + ", id_token='" 100 | + id_token 101 | + '\'' 102 | + '}'; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/pkce/CodeChallengeError.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.pkce; 2 | 3 | public class CodeChallengeError extends Exception { 4 | public CodeChallengeError() { 5 | super("PKCE: Code challenge failed"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/pkce/ProofKeyForCodeExchangeVerifier.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.pkce; 2 | 3 | public interface ProofKeyForCodeExchangeVerifier { 4 | 5 | String CHALLENGE_METHOD_S_256 = "S256"; 6 | String CHALLENGE_METHOD_PLAIN = "plain"; 7 | 8 | /** 9 | * @param challengeMethod OPTIONAL, defaults to "plain" if not present in the request. Code 10 | * verifier transformation method is "S256" or "plain". 11 | * @param codeVerifier high-entropy cryptographic random STRING using the unreserved characters 12 | * [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" from Section 2.3 of [RFC3986], with a minimum 13 | * length of 43 characters and a maximum length of 128 characters. 14 | * @param codeChallenge The client creates a code challenge derived from the code verifier by 15 | * using one of the following transformations on the code verifier: 16 | *

plain code_challenge = code_verifier 17 | *

S256 code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) 18 | * 19 | * @throws CodeChallengeError if verification of code challenge with code verifier fails 20 | */ 21 | void verifyCodeChallenge(String challengeMethod, String codeVerifier, String codeChallenge) 22 | throws CodeChallengeError; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/pkce/ProofKeyForCodeExchangeVerifierStandardImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.pkce; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.security.MessageDigest; 9 | import java.security.NoSuchAlgorithmException; 10 | import java.util.Base64; 11 | 12 | import static java.nio.charset.StandardCharsets.UTF_8; 13 | 14 | @Service 15 | public class ProofKeyForCodeExchangeVerifierStandardImpl implements ProofKeyForCodeExchangeVerifier { 16 | 17 | private static final Logger LOG = LoggerFactory.getLogger(ProofKeyForCodeExchangeVerifierStandardImpl.class); 18 | 19 | @Override 20 | public void verifyCodeChallenge(String challengeMethod, String codeVerifier, String codeChallenge) throws CodeChallengeError { 21 | 22 | LOG.debug("Verifying PKCE code challenge with code verifier using method [{}]", challengeMethod); 23 | 24 | if (StringUtils.isBlank(codeVerifier)) { 25 | LOG.warn("Code verifier must not be empty"); 26 | throw new CodeChallengeError(); 27 | } 28 | 29 | if (codeVerifier.length() < 43 || codeVerifier.length() > 128) { 30 | LOG.warn("Code verifier must have a length between 43 and 128 characters"); 31 | throw new CodeChallengeError(); 32 | } 33 | 34 | if (CHALLENGE_METHOD_S_256.equalsIgnoreCase(challengeMethod)) { 35 | // Rehash the code verifier 36 | try { 37 | String rehashedChallenge = rehashCodeVerifier(codeVerifier); 38 | if (!MessageDigest.isEqual( 39 | codeChallenge.getBytes(UTF_8), rehashedChallenge.getBytes(UTF_8))) { 40 | throw new CodeChallengeError(); 41 | } 42 | } catch (NoSuchAlgorithmException e) { 43 | throw new CodeChallengeError(); 44 | } 45 | } else if (challengeMethod == null || challengeMethod.isBlank() || CHALLENGE_METHOD_PLAIN.equalsIgnoreCase(challengeMethod)) { 46 | if (!codeChallenge.equals(codeVerifier)) { 47 | throw new CodeChallengeError(); 48 | } 49 | } else { 50 | LOG.warn("Invalid Code Challenge [{}]", codeChallenge); 51 | throw new CodeChallengeError(); 52 | } 53 | } 54 | 55 | private String rehashCodeVerifier(String codeVerifier) throws NoSuchAlgorithmException { 56 | final MessageDigest digest = MessageDigest.getInstance("SHA-256"); 57 | final byte[] hashedBytes = digest.digest(codeVerifier.getBytes(UTF_8)); 58 | return Base64.getUrlEncoder().withoutPadding().encodeToString(hashedBytes); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/store/AuthorizationCode.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.store; 2 | 3 | import java.net.URI; 4 | import java.time.LocalDateTime; 5 | import java.util.Set; 6 | 7 | public class AuthorizationCode { 8 | 9 | private final String clientId; 10 | private final URI redirectUri; 11 | private final Set scopes; 12 | private final String code; 13 | private final LocalDateTime expiry; 14 | private final String subject; 15 | private final String nonce; 16 | private final String code_challenge; 17 | private final String code_challenge_method; 18 | 19 | public AuthorizationCode( 20 | String clientId, 21 | URI redirectUri, 22 | Set scopes, 23 | String code, 24 | String subject, 25 | String nonce, 26 | String code_challenge, 27 | String code_challenge_method) { 28 | this.clientId = clientId; 29 | this.redirectUri = redirectUri; 30 | this.scopes = scopes; 31 | this.code = code; 32 | this.subject = subject; 33 | this.nonce = nonce; 34 | this.code_challenge = code_challenge; 35 | this.code_challenge_method = code_challenge_method; 36 | this.expiry = LocalDateTime.now().plusMinutes(2); 37 | } 38 | 39 | public String getClientId() { 40 | return clientId; 41 | } 42 | 43 | public String getSubject() { 44 | return subject; 45 | } 46 | 47 | public String getNonce() { 48 | return nonce; 49 | } 50 | 51 | public URI getRedirectUri() { 52 | return redirectUri; 53 | } 54 | 55 | public String getCode() { 56 | return code; 57 | } 58 | 59 | public LocalDateTime getExpiry() { 60 | return expiry; 61 | } 62 | 63 | public Set getScopes() { 64 | return scopes; 65 | } 66 | 67 | public String getCode_challenge() { 68 | return code_challenge; 69 | } 70 | 71 | public String getCode_challenge_method() { 72 | return code_challenge_method; 73 | } 74 | 75 | public boolean isExpired() { 76 | return LocalDateTime.now().isAfter(getExpiry()); 77 | } 78 | 79 | @Override 80 | public String toString() { 81 | return "AuthorizationState{" 82 | + "clientId='" 83 | + clientId 84 | + '\'' 85 | + ", redirectUri=" 86 | + redirectUri 87 | + ", scopes=" 88 | + scopes 89 | + ", code='" 90 | + code 91 | + '\'' 92 | + ", expiry=" 93 | + expiry 94 | + ", subject=" 95 | + subject 96 | + ", nonce=" 97 | + nonce 98 | + ", code_challenge=" 99 | + code_challenge 100 | + ", code_challenge_method=" 101 | + code_challenge_method 102 | + '}'; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oauth/store/AuthorizationCodeService.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oauth.store; 2 | 3 | import org.apache.commons.lang3.RandomStringUtils; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.net.URI; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | @Service 12 | public class AuthorizationCodeService { 13 | 14 | private final Map codeMap = new HashMap<>(); 15 | 16 | public AuthorizationCode getCode(String code) { 17 | return codeMap.get(code); 18 | } 19 | 20 | public AuthorizationCode createAndStoreAuthorizationState( 21 | String clientId, 22 | URI redirectUri, 23 | Set scopes, 24 | String subject, 25 | String nonce, 26 | String code_challenge, 27 | String code_challenge_method) { 28 | String code = RandomStringUtils.random(32, true, true); 29 | AuthorizationCode authorizationCode = 30 | new AuthorizationCode( 31 | clientId, 32 | redirectUri, 33 | scopes, 34 | code, 35 | subject, 36 | nonce, 37 | code_challenge, 38 | code_challenge_method); 39 | codeMap.put(code, authorizationCode); 40 | return authorizationCode; 41 | } 42 | 43 | public void removeCode(String code) { 44 | codeMap.remove(code); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oidc/common/Scope.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oidc.common; 2 | 3 | public enum Scope { 4 | OPENID, 5 | OFFLINE_ACCESS, 6 | EMAIL, 7 | ADDRESS, 8 | PHONE, 9 | PROFILE 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oidc/endpoint/discovery/DiscoveryEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oidc.endpoint.discovery; 2 | 3 | import com.example.authorizationserver.jwks.JwtPki; 4 | import com.example.authorizationserver.oauth.common.GrantType; 5 | import com.example.authorizationserver.oauth.endpoint.AuthorizationEndpoint; 6 | import com.example.authorizationserver.oauth.endpoint.introspection.IntrospectionEndpoint; 7 | import com.example.authorizationserver.oauth.endpoint.revocation.RevocationEndpoint; 8 | import com.example.authorizationserver.oauth.endpoint.token.TokenEndpoint; 9 | import com.example.authorizationserver.oidc.common.Scope; 10 | import com.example.authorizationserver.oidc.endpoint.userinfo.UserInfoEndpoint; 11 | import io.swagger.v3.oas.annotations.Operation; 12 | import org.springframework.web.bind.annotation.CrossOrigin; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | @CrossOrigin(originPatterns = "*", allowCredentials = "true", allowedHeaders = "*") 18 | @RestController 19 | @RequestMapping(DiscoveryEndpoint.ENDPOINT) 20 | public class DiscoveryEndpoint { 21 | 22 | public static final String ENDPOINT = "/.well-known/openid-configuration"; 23 | 24 | private final JwtPki jwtPki; 25 | 26 | public DiscoveryEndpoint(JwtPki jwtPki) { 27 | this.jwtPki = jwtPki; 28 | } 29 | 30 | @Operation( 31 | summary = "Retrieves the public OpenID Connect configuration", 32 | tags = {"OpenID Connect Discovery"} 33 | ) 34 | @GetMapping 35 | public Discovery discoveryEndpoint() { 36 | 37 | Discovery discovery = new Discovery(); 38 | discovery.setAuthorization_endpoint(jwtPki.getIssuer() + AuthorizationEndpoint.ENDPOINT); 39 | discovery.setIssuer(jwtPki.getIssuer()); 40 | discovery.setToken_endpoint(jwtPki.getIssuer() + TokenEndpoint.ENDPOINT); 41 | discovery.setIntrospection_endpoint(jwtPki.getIssuer() + IntrospectionEndpoint.ENDPOINT); 42 | discovery.setRevocation_endpoint(jwtPki.getIssuer() + RevocationEndpoint.ENDPOINT); 43 | discovery.setUserinfo_endpoint(jwtPki.getIssuer() + UserInfoEndpoint.ENDPOINT); 44 | discovery.setJwks_uri(jwtPki.getIssuer() + "/jwks"); 45 | discovery.getGrant_types_supported().add(GrantType.AUTHORIZATION_CODE.getGrant()); 46 | discovery.getGrant_types_supported().add(GrantType.CLIENT_CREDENTIALS.getGrant()); 47 | discovery.getGrant_types_supported().add(GrantType.PASSWORD.getGrant()); 48 | discovery.getGrant_types_supported().add(GrantType.TOKEN_EXCHANGE.getGrant()); 49 | discovery.getResponse_types_supported().add("code"); 50 | discovery.getScopes_supported().add(Scope.OPENID.name().toLowerCase()); 51 | discovery.getScopes_supported().add(Scope.OFFLINE_ACCESS.name().toLowerCase()); 52 | discovery.getScopes_supported().add(Scope.PROFILE.name().toLowerCase()); 53 | discovery.getScopes_supported().add(Scope.EMAIL.name().toLowerCase()); 54 | discovery.getScopes_supported().add(Scope.PHONE.name().toLowerCase()); 55 | discovery.getScopes_supported().add(Scope.ADDRESS.name().toLowerCase()); 56 | discovery.getResponse_modes_supported().add("query"); 57 | discovery.getResponse_modes_supported().add("form_post"); 58 | discovery.getSubject_types_supported().add("public"); 59 | discovery.getId_token_signing_alg_values_supported().add("RS256"); 60 | discovery.getToken_endpoint_auth_methods_supported().add("client_secret_basic"); 61 | discovery.getToken_endpoint_auth_methods_supported().add("client_secret_post"); 62 | discovery.getCode_challenge_methods_supported().add("S256"); 63 | discovery.getCode_challenge_methods_supported().add("plain"); 64 | discovery.getClaims_supported().add("aud"); 65 | discovery.getClaims_supported().add("auth_time"); 66 | discovery.getClaims_supported().add("created_at"); 67 | discovery.getClaims_supported().add("gender"); 68 | discovery.getClaims_supported().add("birthdate"); 69 | discovery.getClaims_supported().add("locale"); 70 | discovery.getClaims_supported().add("zoneinfo"); 71 | discovery.getClaims_supported().add("address"); 72 | discovery.getClaims_supported().add("email"); 73 | discovery.getClaims_supported().add("email_verified"); 74 | discovery.getClaims_supported().add("exp"); 75 | discovery.getClaims_supported().add("website"); 76 | discovery.getClaims_supported().add("picture"); 77 | discovery.getClaims_supported().add("family_name"); 78 | discovery.getClaims_supported().add("given_name"); 79 | discovery.getClaims_supported().add("iat"); 80 | discovery.getClaims_supported().add("identities"); 81 | discovery.getClaims_supported().add("iss"); 82 | discovery.getClaims_supported().add("identities"); 83 | discovery.getClaims_supported().add("name"); 84 | discovery.getClaims_supported().add("nickname"); 85 | discovery.getClaims_supported().add("phone_number"); 86 | discovery.getClaims_supported().add("phone_number_verified"); 87 | discovery.getClaims_supported().add("sub"); 88 | discovery.getToken_endpoint_auth_signing_alg_values_supported().add("RS256"); 89 | 90 | return discovery; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/oidc/endpoint/userinfo/UserInfoEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oidc.endpoint.userinfo; 2 | 3 | import com.example.authorizationserver.oauth.common.AuthenticationUtil; 4 | import com.example.authorizationserver.scim.model.ScimUserEntity; 5 | import com.example.authorizationserver.scim.service.ScimService; 6 | import com.example.authorizationserver.token.jwt.JsonWebTokenService; 7 | import com.example.authorizationserver.token.store.TokenService; 8 | import com.example.authorizationserver.token.store.model.JsonWebToken; 9 | import com.example.authorizationserver.token.store.model.OpaqueToken; 10 | import com.nimbusds.jose.JOSEException; 11 | import com.nimbusds.jwt.JWTClaimsSet; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.web.bind.MissingRequestHeaderException; 17 | import org.springframework.web.bind.annotation.*; 18 | 19 | import java.text.ParseException; 20 | import java.util.Optional; 21 | import java.util.UUID; 22 | 23 | @CrossOrigin(originPatterns = "*", allowCredentials = "true", allowedHeaders = "*") 24 | @RestController 25 | @RequestMapping(UserInfoEndpoint.ENDPOINT) 26 | public class UserInfoEndpoint { 27 | private static final Logger LOG = 28 | LoggerFactory.getLogger(UserInfoEndpoint.class); 29 | 30 | public static final String ENDPOINT = "/userinfo"; 31 | 32 | private final TokenService tokenService; 33 | private final ScimService scimService; 34 | private final JsonWebTokenService jsonWebTokenService; 35 | 36 | public UserInfoEndpoint( 37 | TokenService tokenService, ScimService scimService, JsonWebTokenService jsonWebTokenService) { 38 | this.tokenService = tokenService; 39 | this.scimService = scimService; 40 | this.jsonWebTokenService = jsonWebTokenService; 41 | } 42 | 43 | @GetMapping 44 | public ResponseEntity userInfo( 45 | @RequestHeader("Authorization") String authorizationHeader) { 46 | String tokenValue = AuthenticationUtil.fromBearerAuthHeader(authorizationHeader); 47 | 48 | LOG.debug("Calling userinfo with bearer token header {}", tokenValue); 49 | 50 | JsonWebToken jsonWebToken = tokenService.findJsonWebToken(tokenValue); 51 | Optional user; 52 | if (jsonWebToken != null) { 53 | try { 54 | JWTClaimsSet jwtClaimsSet = 55 | jsonWebTokenService.parseAndValidateToken(jsonWebToken.getValue()); 56 | if (TokenService.ANONYMOUS_TOKEN.equals(jwtClaimsSet.getStringClaim("ctx"))) { 57 | return ResponseEntity.ok(new UserInfo(jwtClaimsSet.getSubject())); 58 | } else { 59 | user = scimService.findUserByIdentifier(UUID.fromString(jwtClaimsSet.getSubject())); 60 | return user.map(u -> ResponseEntity.ok(new UserInfo(u))) 61 | .orElse( 62 | ResponseEntity.status(HttpStatus.UNAUTHORIZED) 63 | .header("WWW-Authenticate", "Bearer") 64 | .build()); 65 | } 66 | } catch (ParseException | JOSEException e) { 67 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED) 68 | .header("WWW-Authenticate", "Bearer") 69 | .body(new UserInfo("invalid_token", "Access Token is invalid")); 70 | } 71 | } else { 72 | OpaqueToken opaqueWebToken = tokenService.findOpaqueToken(tokenValue); 73 | if (opaqueWebToken != null) { 74 | opaqueWebToken.validate(); 75 | if (TokenService.ANONYMOUS_TOKEN.equals(opaqueWebToken.getSubject())) { 76 | return ResponseEntity.ok(new UserInfo(opaqueWebToken.getSubject())); 77 | } else { 78 | user = scimService.findUserByIdentifier(UUID.fromString(opaqueWebToken.getSubject())); 79 | return user.map(u -> ResponseEntity.ok(new UserInfo(u))) 80 | .orElse( 81 | ResponseEntity.status(HttpStatus.UNAUTHORIZED) 82 | .header("WWW-Authenticate", "Bearer") 83 | .build()); 84 | } 85 | } else { 86 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED) 87 | .header("WWW-Authenticate", "Bearer") 88 | .body(new UserInfo("invalid_token", "Access Token is invalid")); 89 | } 90 | } 91 | } 92 | 93 | @ExceptionHandler(MissingRequestHeaderException.class) 94 | public ResponseEntity handle(MissingRequestHeaderException ex) { 95 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED) 96 | .header("WWW-Authenticate", "Bearer") 97 | .body(new UserInfo("invalid_token", "Access Token is required")); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/AddressResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource; 2 | 3 | import com.example.authorizationserver.scim.model.ScimAddressEntity; 4 | 5 | import javax.validation.constraints.NotBlank; 6 | import javax.validation.constraints.Size; 7 | 8 | public class AddressResource { 9 | 10 | @NotBlank 11 | @Size(max = 100) 12 | private String street; 13 | 14 | @NotBlank 15 | @Size(max = 20) 16 | private String zip; 17 | 18 | @NotBlank 19 | @Size(max = 100) 20 | private String city; 21 | 22 | @Size(max = 100) 23 | private String state; 24 | 25 | @NotBlank 26 | @Size(max = 100) 27 | private String country; 28 | 29 | public AddressResource() {} 30 | 31 | public AddressResource(ScimAddressEntity address) { 32 | this.city = address.getLocality(); 33 | this.country = address.getCountry(); 34 | this.state = address.getRegion(); 35 | this.street = address.getStreetAddress(); 36 | this.zip = address.getPostalCode(); 37 | } 38 | 39 | public String getStreet() { 40 | return street; 41 | } 42 | 43 | public void setStreet(String street) { 44 | this.street = street; 45 | } 46 | 47 | public String getZip() { 48 | return zip; 49 | } 50 | 51 | public void setZip(String zip) { 52 | this.zip = zip; 53 | } 54 | 55 | public String getCity() { 56 | return city; 57 | } 58 | 59 | public void setCity(String city) { 60 | this.city = city; 61 | } 62 | 63 | public String getState() { 64 | return state; 65 | } 66 | 67 | public void setState(String state) { 68 | this.state = state; 69 | } 70 | 71 | public String getCountry() { 72 | return country; 73 | } 74 | 75 | public void setCountry(String country) { 76 | this.country = country; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/CreateScimUserResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | import java.net.URI; 11 | import java.util.Set; 12 | import java.util.UUID; 13 | 14 | public class CreateScimUserResource extends ScimUserResource { 15 | 16 | @NotNull 17 | @NotBlank 18 | @Size(min = 8, max = 255) 19 | private String password; 20 | 21 | public CreateScimUserResource() { 22 | } 23 | 24 | public CreateScimUserResource(ScimMetaResource meta, UUID identifier, String externalId, String userName, 25 | String familyName, String givenName, String middleName, String honorificPrefix, 26 | String honorificSuffix, String nickName, URI profileUrl, String title, String userType, 27 | String preferredLanguage, String locale, String timezone, boolean active, String password, 28 | Set emails, Set phoneNumbers, 29 | Set ims, Set photos, Set addresses, 30 | Set groups, Set entitlements, Set roles, 31 | Set x509Certificates) { 32 | super(meta, identifier, externalId, userName, familyName, givenName, middleName, honorificPrefix, honorificSuffix, nickName, profileUrl, title, userType, preferredLanguage, locale, timezone, active, emails, phoneNumbers, ims, photos, addresses, groups, entitlements, roles, x509Certificates); 33 | this.password = password; 34 | } 35 | 36 | public String getPassword() { 37 | return password; 38 | } 39 | 40 | public void setPassword(String password) { 41 | this.password = password; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return new ToStringBuilder(this) 47 | .appendSuper(super.toString()) 48 | .append("password", password) 49 | .toString(); 50 | } 51 | 52 | @Override 53 | public boolean equals(Object o) { 54 | if (this == o) return true; 55 | 56 | if (o == null || getClass() != o.getClass()) return false; 57 | 58 | CreateScimUserResource that = (CreateScimUserResource) o; 59 | 60 | return new EqualsBuilder() 61 | .appendSuper(super.equals(o)) 62 | .append(password, that.password) 63 | .isEquals(); 64 | } 65 | 66 | @Override 67 | public int hashCode() { 68 | return new HashCodeBuilder(17, 37) 69 | .appendSuper(super.hashCode()) 70 | .append(password) 71 | .toHashCode(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/ScimAddressResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.apache.commons.lang3.builder.EqualsBuilder; 5 | import org.apache.commons.lang3.builder.HashCodeBuilder; 6 | import org.apache.commons.lang3.builder.ToStringBuilder; 7 | 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Pattern; 10 | import javax.validation.constraints.Size; 11 | import java.io.Serializable; 12 | 13 | public class ScimAddressResource implements Serializable { 14 | 15 | @Size(max = 100) 16 | private String streetAddress; 17 | 18 | @Size(max = 100) 19 | private String locality; 20 | 21 | @Size(max = 100) 22 | private String region; 23 | 24 | @Size(max = 100) 25 | private String postalCode; 26 | 27 | @Size(max = 2) 28 | @Pattern(regexp = "^[A-Z]{2}$") 29 | private String country; 30 | 31 | @Size(max = 100) 32 | private String type; 33 | 34 | @NotNull 35 | private boolean primary; 36 | 37 | public ScimAddressResource() { 38 | } 39 | 40 | public ScimAddressResource(String streetAddress, String locality, String region, String postalCode, String country, String type, boolean primary) { 41 | this.streetAddress = streetAddress; 42 | this.locality = locality; 43 | this.region = region; 44 | this.postalCode = postalCode; 45 | this.country = country; 46 | this.type = type; 47 | this.primary = primary; 48 | } 49 | 50 | public String formatted() { 51 | return "" 52 | + (StringUtils.isNotBlank(streetAddress) ? streetAddress + "\n" : "") 53 | + (StringUtils.isNotBlank(locality) ? locality + " " : "") 54 | + (StringUtils.isNotBlank(postalCode) ? postalCode + "\n" : "") 55 | + (StringUtils.isNotBlank(country) ? country : ""); 56 | } 57 | 58 | public String getStreetAddress() { 59 | return streetAddress; 60 | } 61 | 62 | public void setStreetAddress(String streetAddress) { 63 | this.streetAddress = streetAddress; 64 | } 65 | 66 | public String getLocality() { 67 | return locality; 68 | } 69 | 70 | public void setLocality(String locality) { 71 | this.locality = locality; 72 | } 73 | 74 | public String getRegion() { 75 | return region; 76 | } 77 | 78 | public void setRegion(String region) { 79 | this.region = region; 80 | } 81 | 82 | public String getPostalCode() { 83 | return postalCode; 84 | } 85 | 86 | public void setPostalCode(String postalCode) { 87 | this.postalCode = postalCode; 88 | } 89 | 90 | public String getCountry() { 91 | return country; 92 | } 93 | 94 | public void setCountry(String country) { 95 | this.country = country; 96 | } 97 | 98 | public String getType() { 99 | return type; 100 | } 101 | 102 | public void setType(String type) { 103 | this.type = type; 104 | } 105 | 106 | public boolean isPrimary() { 107 | return primary; 108 | } 109 | 110 | public void setPrimary(boolean primary) { 111 | this.primary = primary; 112 | } 113 | 114 | @Override 115 | public String toString() { 116 | return new ToStringBuilder(this) 117 | .append("streetAddress", streetAddress) 118 | .append("locality", locality) 119 | .append("region", region) 120 | .append("postalCode", postalCode) 121 | .append("country", country) 122 | .append("type", type) 123 | .append("primary", primary) 124 | .toString(); 125 | } 126 | 127 | @Override 128 | public boolean equals(Object o) { 129 | if (this == o) return true; 130 | 131 | if (o == null || getClass() != o.getClass()) return false; 132 | 133 | ScimAddressResource that = (ScimAddressResource) o; 134 | 135 | return new EqualsBuilder() 136 | .appendSuper(super.equals(o)) 137 | .append(primary, that.primary) 138 | .append(streetAddress, that.streetAddress) 139 | .append(locality, that.locality) 140 | .append(region, that.region) 141 | .append(postalCode, that.postalCode) 142 | .append(country, that.country) 143 | .append(type, that.type) 144 | .isEquals(); 145 | } 146 | 147 | @Override 148 | public int hashCode() { 149 | return new HashCodeBuilder(17, 37) 150 | .appendSuper(super.hashCode()) 151 | .append(streetAddress) 152 | .append(locality) 153 | .append(region) 154 | .append(postalCode) 155 | .append(country) 156 | .append(type) 157 | .append(primary) 158 | .toHashCode(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/ScimEmailResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import javax.validation.constraints.Email; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | public class ScimEmailResource implements Serializable { 12 | 13 | @NotNull 14 | @Email 15 | private String value; 16 | 17 | @NotNull 18 | private String type; 19 | 20 | private boolean primary; 21 | 22 | public ScimEmailResource() { 23 | } 24 | 25 | public ScimEmailResource(String value, String type, boolean primary) { 26 | this.value = value; 27 | this.type = type; 28 | this.primary = primary; 29 | } 30 | 31 | public String getValue() { 32 | return value; 33 | } 34 | 35 | public void setValue(String value) { 36 | this.value = value; 37 | } 38 | 39 | public boolean isPrimary() { 40 | return primary; 41 | } 42 | 43 | public void setPrimary(boolean primary) { 44 | this.primary = primary; 45 | } 46 | 47 | public String getType() { 48 | return type; 49 | } 50 | 51 | public void setType(String type) { 52 | this.type = type; 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return new ToStringBuilder(this) 58 | .append("value", value) 59 | .append("type", type) 60 | .append("primary", primary) 61 | .toString(); 62 | } 63 | 64 | @Override 65 | public boolean equals(Object o) { 66 | if (this == o) return true; 67 | 68 | if (o == null || getClass() != o.getClass()) return false; 69 | 70 | ScimEmailResource that = (ScimEmailResource) o; 71 | 72 | return new EqualsBuilder() 73 | .appendSuper(super.equals(o)) 74 | .append(primary, that.primary) 75 | .append(value, that.value) 76 | .append(type, that.type) 77 | .isEquals(); 78 | } 79 | 80 | @Override 81 | public int hashCode() { 82 | return new HashCodeBuilder(17, 37) 83 | .appendSuper(super.hashCode()) 84 | .append(value) 85 | .append(type) 86 | .append(primary) 87 | .toHashCode(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/ScimGroupListResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import javax.validation.constraints.NotEmpty; 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | public class ScimGroupListResource extends ScimResource { 14 | 15 | public static final String SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"; 16 | 17 | @NotNull 18 | @NotEmpty 19 | @Size(min = 1, max = 255) 20 | private String displayName; 21 | 22 | public ScimGroupListResource() { 23 | } 24 | 25 | public ScimGroupListResource(ScimMetaResource meta, UUID identifier, String externalId, String displayName) { 26 | super(List.of(SCIM_GROUP_SCHEMA), meta, identifier, externalId); 27 | this.displayName = displayName; 28 | } 29 | 30 | public String getDisplayName() { 31 | return displayName; 32 | } 33 | 34 | public void setDisplayName(String displayName) { 35 | this.displayName = displayName; 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return new ToStringBuilder(this) 41 | .appendSuper(super.toString()) 42 | .append("displayName", displayName) 43 | .toString(); 44 | } 45 | 46 | @Override 47 | public boolean equals(Object o) { 48 | if (this == o) return true; 49 | 50 | if (o == null || getClass() != o.getClass()) return false; 51 | 52 | ScimGroupListResource that = (ScimGroupListResource) o; 53 | 54 | return new EqualsBuilder() 55 | .appendSuper(super.equals(o)) 56 | .append(displayName, that.displayName) 57 | .isEquals(); 58 | } 59 | 60 | @Override 61 | public int hashCode() { 62 | return new HashCodeBuilder(17, 37) 63 | .appendSuper(super.hashCode()) 64 | .append(displayName) 65 | .toHashCode(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/ScimGroupResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | import java.util.UUID; 10 | 11 | public class ScimGroupResource extends ScimGroupListResource { 12 | 13 | private Set members = new HashSet<>(); 14 | 15 | public ScimGroupResource() { 16 | } 17 | 18 | public ScimGroupResource(ScimMetaResource meta, UUID identifier, String externalId, String displayName, Set members) { 19 | super(meta, identifier, externalId, displayName); 20 | this.members = members; 21 | } 22 | 23 | public Set getMembers() { 24 | return members; 25 | } 26 | 27 | public void setMembers(Set members) { 28 | this.members = members; 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return new ToStringBuilder(this) 34 | .appendSuper(super.toString()) 35 | .append("members", members) 36 | .toString(); 37 | } 38 | 39 | @Override 40 | public boolean equals(Object o) { 41 | if (this == o) return true; 42 | 43 | if (o == null || getClass() != o.getClass()) return false; 44 | 45 | ScimGroupResource that = (ScimGroupResource) o; 46 | 47 | return new EqualsBuilder() 48 | .appendSuper(super.equals(o)) 49 | .append(members, that.members) 50 | .isEquals(); 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | return new HashCodeBuilder(17, 37) 56 | .appendSuper(super.hashCode()) 57 | .append(members) 58 | .toHashCode(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/ScimImsResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import javax.validation.constraints.NotNull; 8 | import javax.validation.constraints.Size; 9 | import java.io.Serializable; 10 | 11 | public class ScimImsResource implements Serializable { 12 | 13 | @NotNull 14 | @Size(min = 5, max = 50) 15 | private String value; 16 | 17 | @NotNull 18 | @Size(min = 1, max = 50) 19 | private String type; 20 | 21 | public ScimImsResource() { 22 | } 23 | 24 | public ScimImsResource(String value, String type) { 25 | this.value = value; 26 | this.type = type; 27 | } 28 | 29 | public String getValue() { 30 | return value; 31 | } 32 | 33 | public void setValue(String value) { 34 | this.value = value; 35 | } 36 | 37 | public String getType() { 38 | return type; 39 | } 40 | 41 | public void setType(String type) { 42 | this.type = type; 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return new ToStringBuilder(this) 48 | .append("value", value) 49 | .append("type", type) 50 | .toString(); 51 | } 52 | 53 | @Override 54 | public boolean equals(Object o) { 55 | if (this == o) return true; 56 | 57 | if (o == null || getClass() != o.getClass()) return false; 58 | 59 | ScimImsResource that = (ScimImsResource) o; 60 | 61 | return new EqualsBuilder() 62 | .append(value, that.value) 63 | .append(type, that.type) 64 | .isEquals(); 65 | } 66 | 67 | @Override 68 | public int hashCode() { 69 | return new HashCodeBuilder(17, 37) 70 | .append(value) 71 | .append(type) 72 | .toHashCode(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/ScimMetaResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import java.io.Serializable; 8 | import java.time.Instant; 9 | import java.time.LocalDateTime; 10 | 11 | public class ScimMetaResource implements Serializable { 12 | 13 | private String resourceType; 14 | 15 | private Instant created; 16 | 17 | private Instant lastModified; 18 | 19 | private String version; 20 | 21 | private String location; 22 | 23 | public ScimMetaResource() { 24 | } 25 | 26 | public ScimMetaResource(String resourceType, Instant created, Instant lastModified, String version, String location) { 27 | this.resourceType = resourceType; 28 | this.created = created; 29 | this.lastModified = lastModified; 30 | this.version = version; 31 | this.location = location; 32 | } 33 | 34 | public String getResourceType() { 35 | return resourceType; 36 | } 37 | 38 | public void setResourceType(String resourceType) { 39 | this.resourceType = resourceType; 40 | } 41 | 42 | public Instant getCreated() { 43 | return created; 44 | } 45 | 46 | public void setCreated(Instant created) { 47 | this.created = created; 48 | } 49 | 50 | public Instant getLastModified() { 51 | return lastModified; 52 | } 53 | 54 | public void setLastModified(Instant lastModified) { 55 | this.lastModified = lastModified; 56 | } 57 | 58 | public String getVersion() { 59 | return version; 60 | } 61 | 62 | public void setVersion(String version) { 63 | this.version = version; 64 | } 65 | 66 | public String getLocation() { 67 | return location; 68 | } 69 | 70 | public void setLocation(String location) { 71 | this.location = location; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return new ToStringBuilder(this) 77 | .append("resourceType", resourceType) 78 | .append("created", created) 79 | .append("lastModified", lastModified) 80 | .append("version", version) 81 | .append("location", location) 82 | .toString(); 83 | } 84 | 85 | @Override 86 | public boolean equals(Object o) { 87 | if (this == o) return true; 88 | 89 | if (o == null || getClass() != o.getClass()) return false; 90 | 91 | ScimMetaResource that = (ScimMetaResource) o; 92 | 93 | return new EqualsBuilder() 94 | .append(resourceType, that.resourceType) 95 | .append(created, that.created) 96 | .append(lastModified, that.lastModified) 97 | .append(version, that.version) 98 | .append(location, that.location) 99 | .isEquals(); 100 | } 101 | 102 | @Override 103 | public int hashCode() { 104 | return new HashCodeBuilder(17, 37) 105 | .append(resourceType) 106 | .append(created) 107 | .append(lastModified) 108 | .append(version) 109 | .append(location) 110 | .toHashCode(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/ScimPhoneNumberResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import javax.validation.constraints.NotNull; 8 | import javax.validation.constraints.Size; 9 | import java.io.Serializable; 10 | 11 | public class ScimPhoneNumberResource implements Serializable { 12 | 13 | @NotNull 14 | @Size(min = 5, max = 50) 15 | private String value; 16 | 17 | @NotNull 18 | @Size(min = 1, max = 50) 19 | private String type; 20 | 21 | public ScimPhoneNumberResource() { 22 | } 23 | 24 | public ScimPhoneNumberResource(String value, String type) { 25 | this.value = value; 26 | this.type = type; 27 | } 28 | 29 | public String getValue() { 30 | return value; 31 | } 32 | 33 | public void setValue(String value) { 34 | this.value = value; 35 | } 36 | 37 | public String getType() { 38 | return type; 39 | } 40 | 41 | public void setType(String type) { 42 | this.type = type; 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return new ToStringBuilder(this) 48 | .append("phone", value) 49 | .append("type", type) 50 | .toString(); 51 | } 52 | 53 | @Override 54 | public boolean equals(Object o) { 55 | if (this == o) return true; 56 | 57 | if (o == null || getClass() != o.getClass()) return false; 58 | 59 | ScimPhoneNumberResource that = (ScimPhoneNumberResource) o; 60 | 61 | return new EqualsBuilder() 62 | .appendSuper(super.equals(o)) 63 | .append(value, that.value) 64 | .append(type, that.type) 65 | .isEquals(); 66 | } 67 | 68 | @Override 69 | public int hashCode() { 70 | return new HashCodeBuilder(17, 37) 71 | .appendSuper(super.hashCode()) 72 | .append(value) 73 | .append(type) 74 | .toHashCode(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/ScimPhotoResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import javax.validation.constraints.NotNull; 8 | import javax.validation.constraints.Size; 9 | import java.io.Serializable; 10 | import java.net.URI; 11 | 12 | public class ScimPhotoResource implements Serializable { 13 | 14 | @NotNull 15 | private URI value; 16 | 17 | @NotNull 18 | @Size(min = 1, max = 50) 19 | private String type; 20 | 21 | public ScimPhotoResource() { 22 | } 23 | 24 | public ScimPhotoResource(URI value, String type) { 25 | this.value = value; 26 | this.type = type; 27 | } 28 | 29 | public URI getValue() { 30 | return value; 31 | } 32 | 33 | public void setValue(URI value) { 34 | this.value = value; 35 | } 36 | 37 | public String getType() { 38 | return type; 39 | } 40 | 41 | public void setType(String type) { 42 | this.type = type; 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return new ToStringBuilder(this) 48 | .append("value", value) 49 | .append("type", type) 50 | .toString(); 51 | } 52 | 53 | @Override 54 | public boolean equals(Object o) { 55 | if (this == o) return true; 56 | 57 | if (o == null || getClass() != o.getClass()) return false; 58 | 59 | ScimPhotoResource that = (ScimPhotoResource) o; 60 | 61 | return new EqualsBuilder() 62 | .append(value, that.value) 63 | .append(type, that.type) 64 | .isEquals(); 65 | } 66 | 67 | @Override 68 | public int hashCode() { 69 | return new HashCodeBuilder(17, 37) 70 | .append(value) 71 | .append(type) 72 | .toHashCode(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/ScimRefResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import javax.validation.constraints.NotEmpty; 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | import java.io.Serializable; 11 | import java.net.URI; 12 | import java.util.UUID; 13 | 14 | public class ScimRefResource implements Serializable { 15 | 16 | @NotNull 17 | private UUID value; 18 | 19 | @NotNull 20 | @NotEmpty 21 | @Size(min = 1, max = 255) 22 | private String display; 23 | 24 | @NotNull 25 | private URI $ref; 26 | 27 | public ScimRefResource() { 28 | } 29 | 30 | public ScimRefResource(UUID value, URI $ref, String display) { 31 | this.value = value; 32 | this.display = display; 33 | this.$ref = $ref; 34 | } 35 | 36 | public UUID getValue() { 37 | return value; 38 | } 39 | 40 | public void setValue(UUID value) { 41 | this.value = value; 42 | } 43 | 44 | public URI get$ref() { 45 | return $ref; 46 | } 47 | 48 | public void set$ref(URI $ref) { 49 | this.$ref = $ref; 50 | } 51 | 52 | public String getDisplay() { 53 | return display; 54 | } 55 | 56 | public void setDisplay(String display) { 57 | this.display = display; 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return new ToStringBuilder(this) 63 | .append("value", value) 64 | .append("display", display) 65 | .append("$ref", $ref) 66 | .toString(); 67 | } 68 | 69 | @Override 70 | public boolean equals(Object o) { 71 | if (this == o) return true; 72 | 73 | if (o == null || getClass() != o.getClass()) return false; 74 | 75 | ScimRefResource that = (ScimRefResource) o; 76 | 77 | return new EqualsBuilder() 78 | .append(value, that.value) 79 | .append(display, that.display) 80 | .append($ref, that.$ref) 81 | .isEquals(); 82 | } 83 | 84 | @Override 85 | public int hashCode() { 86 | return new HashCodeBuilder(17, 37) 87 | .append(value) 88 | .append(display) 89 | .append($ref) 90 | .toHashCode(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/ScimResource.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource; 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import org.apache.commons.lang3.builder.ToStringBuilder; 6 | 7 | import javax.validation.Valid; 8 | import javax.validation.constraints.NotEmpty; 9 | import javax.validation.constraints.NotNull; 10 | import javax.validation.constraints.Size; 11 | import java.io.Serializable; 12 | import java.lang.reflect.Array; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.UUID; 16 | 17 | public abstract class ScimResource implements Serializable { 18 | 19 | @NotNull 20 | @NotEmpty 21 | private List schemas = new ArrayList<>(); 22 | 23 | @Valid 24 | private ScimMetaResource meta; 25 | 26 | private UUID identifier; 27 | 28 | @NotNull 29 | @NotEmpty 30 | @Size(min = 1, max = 50) 31 | private String externalId; 32 | 33 | public ScimResource() { 34 | } 35 | 36 | public ScimResource(List schemas, ScimMetaResource meta, UUID identifier, String externalId) { 37 | this.schemas = schemas; 38 | this.meta = meta; 39 | this.identifier = identifier; 40 | this.externalId = externalId; 41 | } 42 | 43 | public UUID getIdentifier() { 44 | return identifier; 45 | } 46 | 47 | public void setIdentifier(UUID identifier) { 48 | this.identifier = identifier; 49 | } 50 | 51 | public String getExternalId() { 52 | return externalId; 53 | } 54 | 55 | public void setExternalId(String externalId) { 56 | this.externalId = externalId; 57 | } 58 | 59 | public List getSchemas() { 60 | return schemas; 61 | } 62 | 63 | public void setSchemas(List schemas) { 64 | this.schemas = schemas; 65 | } 66 | 67 | public ScimMetaResource getMeta() { 68 | return meta; 69 | } 70 | 71 | public void setMeta(ScimMetaResource meta) { 72 | this.meta = meta; 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | return new ToStringBuilder(this) 78 | .append("schemas", schemas) 79 | .append("meta", meta) 80 | .append("identifier", identifier) 81 | .append("externalId", externalId) 82 | .toString(); 83 | } 84 | 85 | @Override 86 | public boolean equals(Object o) { 87 | if (this == o) return true; 88 | 89 | if (o == null || getClass() != o.getClass()) return false; 90 | 91 | ScimResource that = (ScimResource) o; 92 | 93 | return new EqualsBuilder() 94 | .append(schemas, that.schemas) 95 | .append(meta, that.meta) 96 | .append(identifier, that.identifier) 97 | .append(externalId, that.externalId) 98 | .isEquals(); 99 | } 100 | 101 | @Override 102 | public int hashCode() { 103 | return new HashCodeBuilder(17, 37) 104 | .append(schemas) 105 | .append(meta) 106 | .append(identifier) 107 | .append(externalId) 108 | .toHashCode(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/mapper/CreateScimUserResourceMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource.mapper; 2 | 3 | import com.example.authorizationserver.scim.api.resource.*; 4 | import com.example.authorizationserver.scim.model.*; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.stream.Collectors; 8 | 9 | @Component 10 | public class CreateScimUserResourceMapper { 11 | 12 | public CreateScimUserResource mapEntityToResource(ScimUserEntity scimUserEntity) { 13 | return new CreateScimUserResource( 14 | new ScimMetaResource("User", null, null, 15 | "0", null), 16 | scimUserEntity.getIdentifier(), scimUserEntity.getExternalId(), scimUserEntity.getUserName(), 17 | scimUserEntity.getFamilyName(), scimUserEntity.getGivenName(), scimUserEntity.getMiddleName(), 18 | scimUserEntity.getHonorificPrefix(), scimUserEntity.getHonorificSuffix(), scimUserEntity.getNickName(), 19 | scimUserEntity.getProfileUrl(), scimUserEntity.getTitle(), scimUserEntity.getUserType(), 20 | scimUserEntity.getPreferredLanguage(), scimUserEntity.getLocale(), scimUserEntity.getTimezone(), 21 | scimUserEntity.isActive(), scimUserEntity.getPassword(), 22 | scimUserEntity.getEmails() != null ? scimUserEntity.getEmails().stream().map(e -> new ScimEmailResource(e.getEmail(), e.getType(), e.isPrimaryEmail())).collect(Collectors.toSet()) : null, 23 | scimUserEntity.getPhoneNumbers() != null ? scimUserEntity.getPhoneNumbers().stream().map(p -> new ScimPhoneNumberResource(p.getPhone(), p.getType())).collect(Collectors.toSet()) : null, 24 | scimUserEntity.getIms() != null ? scimUserEntity.getIms().stream().map(i -> new ScimImsResource(i.getIms(), i.getType())).collect(Collectors.toSet()) : null, 25 | scimUserEntity.getPhotos() != null ? scimUserEntity.getPhotos().stream().map(p -> new ScimPhotoResource(p.getPhotoUrl(), p.getType())).collect(Collectors.toSet()) : null, 26 | scimUserEntity.getAddresses() != null ? scimUserEntity.getAddresses().stream().map(a -> new ScimAddressResource(a.getStreetAddress(), a.getLocality(), a.getRegion(), a.getPostalCode(), a.getCountry(), a.getType(), a.isPrimaryAddress())).collect(Collectors.toSet()) : null, 27 | scimUserEntity.getGroups() != null ? scimUserEntity.getGroups().stream().map(g -> 28 | new ScimRefResource(g.getGroup().getIdentifier(), null, g.getGroup().getDisplayName()) 29 | ).collect(Collectors.toSet()) : null, 30 | scimUserEntity.getEntitlements(), scimUserEntity.getRoles(), scimUserEntity.getX509Certificates()); 31 | } 32 | 33 | public ScimUserEntity mapResourceToEntity(CreateScimUserResource createScimUserResource) { 34 | 35 | return new ScimUserEntity(createScimUserResource.getIdentifier(), 36 | createScimUserResource.getExternalId(), createScimUserResource.getUserName(), createScimUserResource.getFamilyName(), 37 | createScimUserResource.getGivenName(), createScimUserResource.getMiddleName(), createScimUserResource.getHonorificPrefix(), createScimUserResource.getHonorificSuffix(), 38 | createScimUserResource.getNickName(), createScimUserResource.getProfileUrl(), createScimUserResource.getTitle(), createScimUserResource.getUserType(), 39 | createScimUserResource.getPreferredLanguage(), createScimUserResource.getLocale(), createScimUserResource.getTimezone(), createScimUserResource.isActive(), 40 | createScimUserResource.getPassword(), 41 | createScimUserResource.getEmails() != null ? createScimUserResource.getEmails().stream().map(e -> new ScimEmailEntity(e.getValue(), e.getType(), e.isPrimary())).collect(Collectors.toSet()) : null, 42 | createScimUserResource.getPhoneNumbers() != null ? createScimUserResource.getPhoneNumbers().stream().map(p -> new ScimPhoneNumberEntity(p.getValue(), p.getType())).collect(Collectors.toSet()) : null, 43 | createScimUserResource.getIms() != null ? createScimUserResource.getIms().stream().map(p -> new ScimImsEntity(p.getValue(), p.getType())).collect(Collectors.toSet()) : null, 44 | createScimUserResource.getPhotos() != null ? createScimUserResource.getPhotos().stream().map(p -> new ScimPhotoEntity(p.getValue(), p.getType())).collect(Collectors.toSet()) : null, 45 | createScimUserResource.getAddresses() != null ? createScimUserResource.getAddresses().stream().map(a -> new ScimAddressEntity(a.getStreetAddress(), a.getLocality(), a.getRegion(), a.getPostalCode(), a.getCountry(), a.getType(), a.isPrimary())).collect(Collectors.toSet()) : null, 46 | createScimUserResource.getGroups() != null ? createScimUserResource.getGroups().stream().map(p -> 47 | new ScimUserGroupEntity(new ScimUserEntity(), new ScimGroupEntity(p.getValue(), null, p.getDisplay(), null))).collect(Collectors.toSet()) : null, 48 | createScimUserResource.getEntitlements(), createScimUserResource.getRoles(), createScimUserResource.getX509Certificates() 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/mapper/ScimGroupListResourceMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource.mapper; 2 | 3 | import com.example.authorizationserver.scim.api.resource.ScimGroupListResource; 4 | import com.example.authorizationserver.scim.api.resource.ScimMetaResource; 5 | import com.example.authorizationserver.scim.model.ScimGroupEntity; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class ScimGroupListResourceMapper { 10 | 11 | public ScimGroupListResource mapEntityToResource(ScimGroupEntity scimGroupEntity, String location) { 12 | return new ScimGroupListResource(new 13 | ScimMetaResource("Group", scimGroupEntity.getCreatedDate(), 14 | scimGroupEntity.getLastModifiedDate(), 15 | scimGroupEntity.getVersion().toString(), location), scimGroupEntity.getIdentifier(), 16 | scimGroupEntity.getExternalId(), scimGroupEntity.getDisplayName()); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/mapper/ScimGroupResourceMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource.mapper; 2 | 3 | import com.example.authorizationserver.scim.api.resource.ScimGroupResource; 4 | import com.example.authorizationserver.scim.api.resource.ScimMetaResource; 5 | import com.example.authorizationserver.scim.api.resource.ScimRefResource; 6 | import com.example.authorizationserver.scim.model.ScimGroupEntity; 7 | import com.example.authorizationserver.scim.model.ScimUserEntity; 8 | import com.example.authorizationserver.scim.model.ScimUserGroupEntity; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 11 | 12 | import java.net.URI; 13 | import java.util.stream.Collectors; 14 | 15 | import static com.example.authorizationserver.scim.api.ScimUserRestController.USER_ENDPOINT; 16 | import static java.util.Collections.emptySet; 17 | 18 | @Component 19 | public class ScimGroupResourceMapper { 20 | 21 | public ScimGroupResource mapEntityToResource(ScimGroupEntity scimGroupEntity, String location) { 22 | return new ScimGroupResource(new 23 | ScimMetaResource("Group", scimGroupEntity.getCreatedDate(), 24 | scimGroupEntity.getLastModifiedDate(), 25 | scimGroupEntity.getVersion().toString(), location), scimGroupEntity.getIdentifier(), 26 | scimGroupEntity.getExternalId(), scimGroupEntity.getDisplayName(), 27 | scimGroupEntity.getMembers() != null ? 28 | scimGroupEntity.getMembers() 29 | .stream().map(uge -> { 30 | URI userLocation = 31 | ServletUriComponentsBuilder.fromCurrentContextPath() 32 | .path(USER_ENDPOINT + "/{userId}") 33 | .buildAndExpand(uge.getUser().getIdentifier()) 34 | .toUri(); 35 | return new ScimRefResource(uge.getUser().getIdentifier(), userLocation, uge.getUser().getDisplayName()); 36 | }).collect(Collectors.toSet()) : emptySet()); 37 | } 38 | 39 | public ScimGroupEntity mapResourceToEntity(ScimGroupResource scimGroupResource) { 40 | return new ScimGroupEntity(scimGroupResource.getIdentifier(), scimGroupResource.getExternalId(), 41 | scimGroupResource.getDisplayName(), 42 | scimGroupResource.getMembers() != null ? 43 | scimGroupResource.getMembers().stream() 44 | .map(gref -> new ScimUserGroupEntity(new ScimUserEntity(gref.getValue()), 45 | new ScimGroupEntity(scimGroupResource.getIdentifier(), scimGroupResource.getExternalId(), 46 | scimGroupResource.getDisplayName(), null))) 47 | .collect(Collectors.toSet()) : emptySet()); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/mapper/ScimUserListResourceMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource.mapper; 2 | 3 | import com.example.authorizationserver.scim.api.resource.ScimMetaResource; 4 | import com.example.authorizationserver.scim.api.resource.ScimUserListResource; 5 | import com.example.authorizationserver.scim.model.ScimUserEntity; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class ScimUserListResourceMapper { 10 | 11 | public ScimUserListResource mapEntityToResource(ScimUserEntity scimUserEntity, String location) { 12 | return new ScimUserListResource( 13 | new ScimMetaResource("User", 14 | scimUserEntity.getCreatedDate(), 15 | scimUserEntity.getLastModifiedDate(), 16 | scimUserEntity.getVersion().toString(), location), 17 | scimUserEntity.getIdentifier(), scimUserEntity.getExternalId(), scimUserEntity.getUserName(), 18 | scimUserEntity.getFamilyName(), scimUserEntity.getGivenName(), scimUserEntity.getMiddleName(), 19 | scimUserEntity.getHonorificPrefix(), scimUserEntity.getHonorificSuffix(), scimUserEntity.getNickName(), 20 | scimUserEntity.getProfileUrl(), scimUserEntity.getTitle(), scimUserEntity.getUserType(), 21 | scimUserEntity.getPreferredLanguage(), scimUserEntity.getLocale(), scimUserEntity.getTimezone(), 22 | scimUserEntity.isActive()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/api/resource/mapper/ScimUserResourceMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.api.resource.mapper; 2 | 3 | import com.example.authorizationserver.scim.api.resource.*; 4 | import com.example.authorizationserver.scim.model.ScimUserEntity; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 7 | 8 | import java.net.URI; 9 | import java.util.stream.Collectors; 10 | 11 | import static com.example.authorizationserver.scim.api.ScimGroupRestController.GROUP_ENDPOINT; 12 | import static java.util.Collections.emptySet; 13 | 14 | @Component 15 | public class ScimUserResourceMapper { 16 | 17 | public ScimUserResource mapEntityToResource(ScimUserEntity scimUserEntity, String location) { 18 | return new ScimUserResource( 19 | new ScimMetaResource("User", null, null, 20 | scimUserEntity.getVersion().toString(), location), 21 | scimUserEntity.getIdentifier(), scimUserEntity.getExternalId(), scimUserEntity.getUserName(), 22 | scimUserEntity.getFamilyName(), scimUserEntity.getGivenName(), scimUserEntity.getMiddleName(), 23 | scimUserEntity.getHonorificPrefix(), scimUserEntity.getHonorificSuffix(), scimUserEntity.getNickName(), 24 | scimUserEntity.getProfileUrl(), scimUserEntity.getTitle(), scimUserEntity.getUserType(), 25 | scimUserEntity.getPreferredLanguage(), scimUserEntity.getLocale(), scimUserEntity.getTimezone(), 26 | scimUserEntity.isActive(), 27 | scimUserEntity.getEmails() != null ? 28 | scimUserEntity.getEmails().stream().map(e -> new ScimEmailResource(e.getEmail(), e.getType(), e.isPrimaryEmail())).collect(Collectors.toSet()) : emptySet(), 29 | scimUserEntity.getPhoneNumbers() != null ? 30 | scimUserEntity.getPhoneNumbers().stream().map(p -> new ScimPhoneNumberResource(p.getPhone(), p.getType())).collect(Collectors.toSet()) : emptySet(), 31 | scimUserEntity.getIms() != null ? 32 | scimUserEntity.getIms().stream().map(i -> new ScimImsResource(i.getIms(), i.getType())).collect(Collectors.toSet()): emptySet(), 33 | scimUserEntity.getPhotos() != null ? 34 | scimUserEntity.getPhotos().stream().map(p -> new ScimPhotoResource(p.getPhotoUrl(), p.getType())).collect(Collectors.toSet()) : emptySet(), 35 | scimUserEntity.getAddresses() != null ? 36 | scimUserEntity.getAddresses().stream().map(a -> new ScimAddressResource(a.getStreetAddress(), a.getLocality(), a.getRegion(), a.getPostalCode(), a.getCountry(), a.getType(), a.isPrimaryAddress())).collect(Collectors.toSet()): emptySet(), 37 | scimUserEntity.getGroups() != null ? scimUserEntity.getGroups().stream().map(g -> { 38 | URI groupLocation = 39 | ServletUriComponentsBuilder.fromCurrentContextPath() 40 | .path(GROUP_ENDPOINT + "/{groupId}") 41 | .buildAndExpand(g.getGroup().getIdentifier()) 42 | .toUri(); 43 | return new ScimRefResource(g.getGroup().getIdentifier(), groupLocation, g.getGroup().getDisplayName()); 44 | }).collect(Collectors.toSet()) : emptySet(), 45 | scimUserEntity.getEntitlements() != null ? scimUserEntity.getEntitlements() : emptySet(), 46 | scimUserEntity.getRoles() != null ? scimUserEntity.getRoles() : emptySet(), 47 | scimUserEntity.getX509Certificates() != null ? scimUserEntity.getX509Certificates() : emptySet()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/dao/ScimGroupEntityRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.dao; 2 | 3 | import com.example.authorizationserver.scim.model.ScimGroupEntity; 4 | import org.springframework.data.jpa.repository.EntityGraph; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.Optional; 8 | import java.util.UUID; 9 | 10 | public interface ScimGroupEntityRepository extends JpaRepository { 11 | 12 | @EntityGraph(attributePaths = {"members", "members.user", "members.group"}) 13 | Optional findOneByIdentifier(UUID identifier); 14 | 15 | void deleteOneByIdentifier(UUID identifier); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/dao/ScimUserEntityRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.dao; 2 | 3 | import com.example.authorizationserver.scim.model.ScimUserEntity; 4 | import org.springframework.data.jpa.repository.EntityGraph; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.Optional; 8 | import java.util.UUID; 9 | 10 | public interface ScimUserEntityRepository extends JpaRepository { 11 | 12 | @EntityGraph(attributePaths = {"emails", "phoneNumbers", "ims", "photos", "addresses", "groups", "groups.group", "roles", "entitlements", "x509Certificates"}) 13 | Optional findOneByIdentifier(UUID identifier); 14 | 15 | @EntityGraph(attributePaths = {"emails", "phoneNumbers", "ims", "photos", "addresses", "groups", "groups.group", "roles", "entitlements", "x509Certificates"}) 16 | Optional findOneByUserName(String username); 17 | 18 | void deleteOneByIdentifier(UUID identifier); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/dao/ScimUserGroupEntityRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.dao; 2 | 3 | import com.example.authorizationserver.scim.model.ScimUserGroupEntity; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.query.Param; 7 | 8 | import java.util.List; 9 | import java.util.UUID; 10 | 11 | public interface ScimUserGroupEntityRepository extends JpaRepository { 12 | 13 | @Query("select ge from ScimUserGroupEntity ge where ge.user.identifier = :userIdentifier and ge.group.identifier = :groupIdentifier") 14 | List findAllBy(@Param("userIdentifier") UUID userIdentifier, @Param("groupIdentifier") UUID groupIdentifier); 15 | 16 | void deleteOneByGroup_IdentifierAndUser_Identifier(UUID userIdentifier, UUID groupIdentifier); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/model/ScimAddressEntity.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.model; 2 | 3 | import org.apache.commons.lang3.builder.ToStringBuilder; 4 | import org.springframework.data.jpa.domain.AbstractPersistable; 5 | 6 | import javax.persistence.Entity; 7 | import javax.validation.constraints.NotNull; 8 | import javax.validation.constraints.Pattern; 9 | import javax.validation.constraints.Size; 10 | import java.io.Serializable; 11 | 12 | @Entity 13 | public class ScimAddressEntity extends AbstractPersistable implements Serializable { 14 | 15 | @Size(max = 100) 16 | private String streetAddress; 17 | 18 | @Size(max = 100) 19 | private String locality; 20 | 21 | @Size(max = 100) 22 | private String region; 23 | 24 | @Size(max = 100) 25 | private String postalCode; 26 | 27 | @Size(max = 2) 28 | @Pattern(regexp = "^[A-Z]{2}$") 29 | private String country; 30 | 31 | @Size(max = 100) 32 | private String type; 33 | 34 | @NotNull 35 | private boolean primaryAddress; 36 | 37 | public ScimAddressEntity() { 38 | } 39 | 40 | public ScimAddressEntity(String streetAddress, String locality, String region, String postalCode, String country, String type, boolean primaryAddress) { 41 | this.streetAddress = streetAddress; 42 | this.locality = locality; 43 | this.region = region; 44 | this.postalCode = postalCode; 45 | this.country = country; 46 | this.type = type; 47 | this.primaryAddress = primaryAddress; 48 | } 49 | 50 | public String getStreetAddress() { 51 | return streetAddress; 52 | } 53 | 54 | public void setStreetAddress(String streetAddress) { 55 | this.streetAddress = streetAddress; 56 | } 57 | 58 | public String getLocality() { 59 | return locality; 60 | } 61 | 62 | public void setLocality(String locality) { 63 | this.locality = locality; 64 | } 65 | 66 | public String getRegion() { 67 | return region; 68 | } 69 | 70 | public void setRegion(String region) { 71 | this.region = region; 72 | } 73 | 74 | public String getPostalCode() { 75 | return postalCode; 76 | } 77 | 78 | public void setPostalCode(String postalCode) { 79 | this.postalCode = postalCode; 80 | } 81 | 82 | public String getCountry() { 83 | return country; 84 | } 85 | 86 | public void setCountry(String country) { 87 | this.country = country; 88 | } 89 | 90 | public String getType() { 91 | return type; 92 | } 93 | 94 | public void setType(String type) { 95 | this.type = type; 96 | } 97 | 98 | public boolean isPrimaryAddress() { 99 | return primaryAddress; 100 | } 101 | 102 | public void setPrimaryAddress(boolean primary) { 103 | this.primaryAddress = primary; 104 | } 105 | 106 | @Override 107 | public String toString() { 108 | return new ToStringBuilder(this) 109 | .appendSuper(super.toString()) 110 | .append("streetAddress", streetAddress) 111 | .append("locality", locality) 112 | .append("region", region) 113 | .append("postalCode", postalCode) 114 | .append("country", country) 115 | .append("type", type) 116 | .append("primary", primaryAddress) 117 | .toString(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/model/ScimEmailEntity.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.model; 2 | 3 | import org.apache.commons.lang3.builder.ToStringBuilder; 4 | import org.springframework.data.jpa.domain.AbstractPersistable; 5 | 6 | import javax.persistence.Entity; 7 | import javax.validation.constraints.Email; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | @Entity 12 | public class ScimEmailEntity extends AbstractPersistable implements Serializable { 13 | 14 | @NotNull 15 | @Email 16 | private String email; 17 | 18 | @NotNull 19 | private String type; 20 | 21 | @NotNull 22 | private boolean primaryEmail; 23 | 24 | public ScimEmailEntity() { 25 | } 26 | 27 | public ScimEmailEntity(@NotNull @Email String email, @NotNull String type, @NotNull boolean primaryEmail) { 28 | this.email = email; 29 | this.type = type; 30 | this.primaryEmail = primaryEmail; 31 | } 32 | 33 | public String getEmail() { 34 | return email; 35 | } 36 | 37 | public void setEmail(String email) { 38 | this.email = email; 39 | } 40 | 41 | public String getType() { 42 | return type; 43 | } 44 | 45 | public void setType(String type) { 46 | this.type = type; 47 | } 48 | 49 | public boolean isPrimaryEmail() { 50 | return primaryEmail; 51 | } 52 | 53 | public void setPrimaryEmail(boolean primary) { 54 | this.primaryEmail = primary; 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return new ToStringBuilder(this) 60 | .appendSuper(super.toString()) 61 | .append("email", email) 62 | .append("type", type) 63 | .append("primary", primaryEmail) 64 | .toString(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/model/ScimGroupEntity.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.model; 2 | 3 | import org.apache.commons.lang3.builder.ToStringBuilder; 4 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 5 | 6 | import javax.persistence.*; 7 | import javax.validation.constraints.NotEmpty; 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | import java.util.UUID; 13 | 14 | @Entity 15 | @EntityListeners(AuditingEntityListener.class) 16 | public class ScimGroupEntity extends ScimResourceEntity { 17 | 18 | @Column(unique = true) 19 | @NotNull 20 | @NotEmpty 21 | @Size(min = 1, max = 255) 22 | private String displayName; 23 | 24 | @OneToMany( 25 | mappedBy = "group", 26 | cascade = CascadeType.ALL, 27 | orphanRemoval = true 28 | ) 29 | private Set members = new HashSet<>(); 30 | 31 | public ScimGroupEntity() { 32 | } 33 | 34 | public ScimGroupEntity(UUID identifier, String externalId, String displayName, Set members) { 35 | super(identifier, externalId); 36 | this.displayName = displayName; 37 | this.members = members; 38 | } 39 | 40 | public String getDisplayName() { 41 | return displayName; 42 | } 43 | 44 | public void setDisplayName(String displayName) { 45 | this.displayName = displayName; 46 | } 47 | 48 | public Set getMembers() { 49 | return members; 50 | } 51 | 52 | public void setMembers(Set members) { 53 | this.members = members; 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return new ToStringBuilder(this) 59 | .appendSuper(super.toString()) 60 | .append("displayName", displayName) 61 | .toString(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/model/ScimImsEntity.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.model; 2 | 3 | import org.apache.commons.lang3.builder.ToStringBuilder; 4 | import org.springframework.data.jpa.domain.AbstractPersistable; 5 | 6 | import javax.persistence.Entity; 7 | import javax.validation.constraints.NotNull; 8 | import javax.validation.constraints.Size; 9 | import java.io.Serializable; 10 | 11 | @Entity 12 | public class ScimImsEntity extends AbstractPersistable implements Serializable { 13 | 14 | @NotNull 15 | @Size(min = 5, max = 50) 16 | private String ims; 17 | 18 | @NotNull 19 | @Size(min = 1, max = 50) 20 | private String type; 21 | 22 | public ScimImsEntity() { 23 | } 24 | 25 | public ScimImsEntity(@NotNull @Size(min = 5, max = 50) String ims, @NotNull @Size(min = 1, max = 50) String type) { 26 | this.ims = ims; 27 | this.type = type; 28 | } 29 | 30 | public String getIms() { 31 | return ims; 32 | } 33 | 34 | public void setIms(String ims) { 35 | this.ims = ims; 36 | } 37 | 38 | public String getType() { 39 | return type; 40 | } 41 | 42 | public void setType(String type) { 43 | this.type = type; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return new ToStringBuilder(this) 49 | .appendSuper(super.toString()) 50 | .append("ims", ims) 51 | .append("type", type) 52 | .toString(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/model/ScimPhoneNumberEntity.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.model; 2 | 3 | import org.apache.commons.lang3.builder.ToStringBuilder; 4 | import org.springframework.data.jpa.domain.AbstractPersistable; 5 | 6 | import javax.persistence.Entity; 7 | import javax.validation.constraints.NotNull; 8 | import javax.validation.constraints.Size; 9 | import java.io.Serializable; 10 | 11 | @Entity 12 | public class ScimPhoneNumberEntity extends AbstractPersistable implements Serializable { 13 | 14 | @NotNull 15 | @Size(min = 5, max = 50) 16 | private String phone; 17 | 18 | @NotNull 19 | @Size(min = 1, max = 50) 20 | private String type; 21 | 22 | public ScimPhoneNumberEntity() { 23 | } 24 | 25 | public ScimPhoneNumberEntity(@NotNull @Size(min = 5, max = 50) String phone, @NotNull @Size(min = 1, max = 50) String type) { 26 | this.phone = phone; 27 | this.type = type; 28 | } 29 | 30 | public String getPhone() { 31 | return phone; 32 | } 33 | 34 | public void setPhone(String phone) { 35 | this.phone = phone; 36 | } 37 | 38 | public String getType() { 39 | return type; 40 | } 41 | 42 | public void setType(String type) { 43 | this.type = type; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return new ToStringBuilder(this) 49 | .appendSuper(super.toString()) 50 | .append("phone", phone) 51 | .append("type", type) 52 | .toString(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/model/ScimPhotoEntity.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.model; 2 | 3 | import org.apache.commons.lang3.builder.ToStringBuilder; 4 | import org.springframework.data.jpa.domain.AbstractPersistable; 5 | 6 | import javax.persistence.Entity; 7 | import javax.validation.constraints.NotNull; 8 | import javax.validation.constraints.Size; 9 | import java.io.Serializable; 10 | import java.net.URI; 11 | 12 | @Entity 13 | public class ScimPhotoEntity extends AbstractPersistable implements Serializable { 14 | 15 | @NotNull 16 | private URI photoUrl; 17 | 18 | @NotNull 19 | @Size(min = 1, max = 50) 20 | private String type; 21 | 22 | public ScimPhotoEntity() { 23 | } 24 | 25 | public ScimPhotoEntity(URI photoUrl, String type) { 26 | this.photoUrl = photoUrl; 27 | this.type = type; 28 | } 29 | 30 | public URI getPhotoUrl() { 31 | return photoUrl; 32 | } 33 | 34 | public void setPhotoUrl(URI photoUrl) { 35 | this.photoUrl = photoUrl; 36 | } 37 | 38 | public String getType() { 39 | return type; 40 | } 41 | 42 | public void setType(String type) { 43 | this.type = type; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return new ToStringBuilder(this) 49 | .appendSuper(super.toString()) 50 | .append("photoUrl", photoUrl) 51 | .append("type", type) 52 | .toString(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/model/ScimResourceEntity.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.model; 2 | 3 | import org.apache.commons.lang3.builder.ToStringBuilder; 4 | import org.springframework.data.annotation.CreatedDate; 5 | import org.springframework.data.annotation.LastModifiedDate; 6 | import org.springframework.data.jpa.domain.AbstractPersistable; 7 | import org.springframework.lang.Nullable; 8 | 9 | import javax.persistence.*; 10 | import javax.validation.constraints.NotNull; 11 | import javax.validation.constraints.Size; 12 | import java.io.Serializable; 13 | import java.time.Instant; 14 | import java.util.Date; 15 | import java.util.UUID; 16 | 17 | @MappedSuperclass 18 | public abstract class ScimResourceEntity extends AbstractPersistable implements Serializable { 19 | 20 | @Version 21 | private Long version; 22 | 23 | @NotNull 24 | private UUID identifier; 25 | 26 | @Size(max = 50) 27 | private String externalId; 28 | 29 | @CreatedDate 30 | private Instant createdDate; 31 | 32 | @LastModifiedDate 33 | private Instant lastModifiedDate; 34 | 35 | public ScimResourceEntity() { 36 | } 37 | 38 | public ScimResourceEntity(UUID identifier, String externalId) { 39 | this.identifier = identifier; 40 | this.externalId = externalId; 41 | } 42 | 43 | public Instant getCreatedDate() { 44 | return createdDate; 45 | } 46 | 47 | public Instant getLastModifiedDate() { 48 | return lastModifiedDate; 49 | } 50 | 51 | public UUID getIdentifier() { 52 | return identifier; 53 | } 54 | 55 | public void setIdentifier(UUID identifier) { 56 | this.identifier = identifier; 57 | } 58 | 59 | public String getExternalId() { 60 | return externalId; 61 | } 62 | 63 | public void setExternalId(String externalId) { 64 | this.externalId = externalId; 65 | } 66 | 67 | public Long getVersion() { 68 | return version; 69 | } 70 | 71 | @Override 72 | public String toString() { 73 | return new ToStringBuilder(this) 74 | .appendSuper(super.toString()) 75 | .append("identifier", identifier) 76 | .append("externalId", externalId) 77 | .append("version", version) 78 | .toString(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/model/ScimUserGroupEntity.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.model; 2 | 3 | import org.apache.commons.lang3.builder.ToStringBuilder; 4 | import org.springframework.data.jpa.domain.AbstractPersistable; 5 | 6 | import javax.persistence.Entity; 7 | import javax.persistence.ManyToOne; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | @Entity 12 | public class ScimUserGroupEntity extends AbstractPersistable implements Serializable { 13 | 14 | @NotNull 15 | @ManyToOne(optional = false) 16 | private ScimUserEntity user; 17 | 18 | @NotNull 19 | @ManyToOne(optional = false) 20 | private ScimGroupEntity group; 21 | 22 | public ScimUserGroupEntity() { 23 | } 24 | 25 | public ScimUserGroupEntity(ScimUserEntity user, ScimGroupEntity group) { 26 | this.user = user; 27 | this.group = group; 28 | } 29 | 30 | public ScimUserEntity getUser() { 31 | return user; 32 | } 33 | 34 | public void setUser(ScimUserEntity user) { 35 | this.user = user; 36 | } 37 | 38 | public ScimGroupEntity getGroup() { 39 | return group; 40 | } 41 | 42 | public void setGroup(ScimGroupEntity group) { 43 | this.group = group; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return new ToStringBuilder(this) 49 | .appendSuper(super.toString()) 50 | .append("user", user) 51 | .append("group", group) 52 | .toString(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/service/ScimGroupNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.service; 2 | 3 | import java.util.UUID; 4 | 5 | public class ScimGroupNotFoundException extends RuntimeException { 6 | 7 | public ScimGroupNotFoundException(UUID groupIdentifier) { 8 | super(String.format("No group found with identifier %s", groupIdentifier)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/service/ScimUserNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.service; 2 | 3 | import java.util.UUID; 4 | 5 | public class ScimUserNotFoundException extends RuntimeException { 6 | 7 | public ScimUserNotFoundException(UUID userIdentifier) { 8 | super(String.format("No user found with identifier %s", userIdentifier)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/scim/web/ScimWebController.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.scim.web; 2 | 3 | import com.example.authorizationserver.scim.api.resource.ScimUserListResource; 4 | import com.example.authorizationserver.scim.api.resource.mapper.ScimUserListResourceMapper; 5 | import com.example.authorizationserver.scim.service.ScimService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.ModelAttribute; 10 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 11 | 12 | import java.net.URI; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | import static com.example.authorizationserver.scim.api.ScimUserRestController.USER_ENDPOINT; 17 | 18 | @Controller 19 | public class ScimWebController { 20 | 21 | private final ScimService scimService; 22 | private final ScimUserListResourceMapper scimUserListResourceMapper; 23 | 24 | @Autowired 25 | public ScimWebController(ScimService scimService, ScimUserListResourceMapper scimUserListResourceMapper) { 26 | this.scimService = scimService; 27 | this.scimUserListResourceMapper = scimUserListResourceMapper; 28 | } 29 | 30 | @ModelAttribute("allUsers") 31 | public List populateUsers() { 32 | return this.scimService.findAllUsers().stream() 33 | .map(u -> { 34 | URI location = 35 | ServletUriComponentsBuilder.fromCurrentContextPath() 36 | .path(USER_ENDPOINT + "/{userId}") 37 | .buildAndExpand(u.getIdentifier()) 38 | .toUri(); 39 | return scimUserListResourceMapper.mapEntityToResource(u, location.toASCIIString()); 40 | }).collect(Collectors.toList()); 41 | } 42 | 43 | @GetMapping("/admin/userlist") 44 | public String findAll() { 45 | return "userlist"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/security/client/RegisteredClientAuthenticationService.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.security.client; 2 | 3 | import com.example.authorizationserver.oauth.client.model.RegisteredClient; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 6 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.AuthenticationException; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.crypto.password.PasswordEncoder; 11 | import org.springframework.stereotype.Service; 12 | 13 | @Service 14 | public class RegisteredClientAuthenticationService { 15 | 16 | private final DaoAuthenticationProvider daoAuthenticationProvider; 17 | 18 | public RegisteredClientAuthenticationService(PasswordEncoder passwordEncoder, 19 | @Qualifier("registeredClientDetailsService") UserDetailsService userDetailsService) { 20 | this.daoAuthenticationProvider = new DaoAuthenticationProvider(); 21 | this.daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); 22 | this.daoAuthenticationProvider.setUserDetailsService(userDetailsService); 23 | } 24 | 25 | public RegisteredClient authenticate(String username, String password) throws AuthenticationException { 26 | UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); 27 | Authentication authentication = this.daoAuthenticationProvider.authenticate(authenticationToken); 28 | return (RegisteredClient) authentication.getPrincipal(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/security/client/RegisteredClientDetails.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.security.client; 2 | 3 | import com.example.authorizationserver.oauth.client.model.RegisteredClient; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.stream.Collectors; 11 | 12 | public class RegisteredClientDetails extends RegisteredClient implements UserDetails { 13 | 14 | public RegisteredClientDetails(RegisteredClient registeredClient) { 15 | super(registeredClient.getIdentifier(),registeredClient.getClientId(), 16 | registeredClient.getClientSecret(),registeredClient.isConfidential(), 17 | registeredClient.getAccessTokenFormat(), registeredClient.getGrantTypes(), 18 | registeredClient.getRedirectUris(), registeredClient.getCorsUris()); 19 | } 20 | 21 | @Override 22 | public Collection getAuthorities() { 23 | Collection authorities = new ArrayList<>(); 24 | authorities.add(new SimpleGrantedAuthority("ROLE_CLIENT")); 25 | authorities.addAll(getGrantTypes().stream() 26 | .map(grantType -> new SimpleGrantedAuthority("ROLE_" + grantType.getGrant().toUpperCase())) 27 | .collect(Collectors.toList())); 28 | return authorities; 29 | } 30 | 31 | @Override 32 | public String getPassword() { 33 | return getClientSecret(); 34 | } 35 | 36 | @Override 37 | public String getUsername() { 38 | return getClientId(); 39 | } 40 | 41 | @Override 42 | public boolean isAccountNonExpired() { 43 | return true; 44 | } 45 | 46 | @Override 47 | public boolean isAccountNonLocked() { 48 | return true; 49 | } 50 | 51 | @Override 52 | public boolean isCredentialsNonExpired() { 53 | return true; 54 | } 55 | 56 | @Override 57 | public boolean isEnabled() { 58 | return true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/security/client/RegisteredClientDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.security.client; 2 | 3 | import com.example.authorizationserver.oauth.client.RegisteredClientService; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | @Qualifier("registeredClientDetailsService") 12 | @Service 13 | public class RegisteredClientDetailsService implements UserDetailsService { 14 | 15 | private final RegisteredClientService registeredClientService; 16 | 17 | public RegisteredClientDetailsService(RegisteredClientService registeredClientService) { 18 | this.registeredClientService = registeredClientService; 19 | } 20 | 21 | @Transactional(readOnly = true) 22 | @Override 23 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 24 | return this.registeredClientService.findOneByClientId(username).map( 25 | RegisteredClientDetails::new 26 | ).orElseThrow(() -> new UsernameNotFoundException(String.format("No client found for '%s'", username))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/security/user/EndUserDetails.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.security.user; 2 | 3 | import com.example.authorizationserver.scim.model.ScimUserEntity; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | 8 | import java.util.Collection; 9 | import java.util.stream.Collectors; 10 | 11 | public class EndUserDetails extends ScimUserEntity implements UserDetails { 12 | 13 | public EndUserDetails(ScimUserEntity user) { 14 | super(user.getIdentifier(), null, user.getUserName(),user.getFamilyName(),user.getGivenName(),user.isActive(), user.getPassword(), 15 | user.getEmails(), user.getPhoneNumbers(), user.getIms(), user.getAddresses(), user.getGroups(), user.getEntitlements(), user.getRoles()); 16 | super.setId(user.getId()); 17 | } 18 | 19 | @Override 20 | public Collection getAuthorities() { 21 | return getRoles().stream() 22 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())).collect(Collectors.toList()); 23 | } 24 | 25 | @Override 26 | public String getUsername() { 27 | return getUserName(); 28 | } 29 | 30 | @Override 31 | public boolean isAccountNonExpired() { 32 | return true; 33 | } 34 | 35 | @Override 36 | public boolean isAccountNonLocked() { 37 | return true; 38 | } 39 | 40 | @Override 41 | public boolean isCredentialsNonExpired() { 42 | return true; 43 | } 44 | 45 | @Override 46 | public boolean isEnabled() { 47 | return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/security/user/EndUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.security.user; 2 | 3 | import com.example.authorizationserver.scim.service.ScimService; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | @Qualifier("endUserDetailsService") 12 | @Service 13 | public class EndUserDetailsService implements UserDetailsService { 14 | 15 | private final ScimService scimService; 16 | 17 | public EndUserDetailsService(ScimService scimService) { 18 | this.scimService = scimService; 19 | } 20 | 21 | @Transactional(readOnly = true) 22 | @Override 23 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 24 | return this.scimService 25 | .findUserByUserName(username) 26 | .map(EndUserDetails::new) 27 | .orElseThrow(() -> new UsernameNotFoundException("No user found")); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/security/user/UserAuthenticationService.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.security.user; 2 | 3 | import com.example.authorizationserver.scim.model.ScimUserEntity; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 6 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.AuthenticationException; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.crypto.password.PasswordEncoder; 11 | import org.springframework.stereotype.Service; 12 | 13 | @Service 14 | public class UserAuthenticationService { 15 | 16 | private final DaoAuthenticationProvider daoAuthenticationProvider; 17 | 18 | public UserAuthenticationService(PasswordEncoder passwordEncoder, 19 | @Qualifier("endUserDetailsService") UserDetailsService userDetailsService) { 20 | this.daoAuthenticationProvider = new DaoAuthenticationProvider(); 21 | this.daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); 22 | this.daoAuthenticationProvider.setUserDetailsService(userDetailsService); 23 | } 24 | 25 | public ScimUserEntity authenticate(String username, String password) throws AuthenticationException { 26 | UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); 27 | Authentication authentication = this.daoAuthenticationProvider.authenticate(authenticationToken); 28 | return (ScimUserEntity) authentication.getPrincipal(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/token/opaque/OpaqueTokenService.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.token.opaque; 2 | 3 | import org.apache.commons.lang3.RandomStringUtils; 4 | import org.springframework.stereotype.Service; 5 | 6 | @Service 7 | public class OpaqueTokenService { 8 | 9 | public String createToken() { 10 | return RandomStringUtils.random(48, true, true); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/token/store/TokenServiceException.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.token.store; 2 | 3 | public class TokenServiceException extends RuntimeException { 4 | 5 | public TokenServiceException(String message) { 6 | super(message); 7 | } 8 | 9 | public TokenServiceException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/token/store/dao/JsonWebTokenRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.token.store.dao; 2 | 3 | import com.example.authorizationserver.token.store.model.JsonWebToken; 4 | 5 | public interface JsonWebTokenRepository extends TokenRepository { 6 | 7 | JsonWebToken findOneByValue(String value); 8 | 9 | JsonWebToken findOneByValueAndAccessToken(String value, boolean accessToken); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/token/store/dao/OpaqueTokenRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.token.store.dao; 2 | 3 | import com.example.authorizationserver.token.store.model.OpaqueToken; 4 | 5 | public interface OpaqueTokenRepository extends TokenRepository { 6 | 7 | OpaqueToken findOneByValue(String value); 8 | 9 | OpaqueToken findOneByValueAndRefreshToken(String value, boolean refreshToken); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/token/store/dao/TokenRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.token.store.dao; 2 | 3 | import com.example.authorizationserver.token.store.model.Token; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface TokenRepository extends JpaRepository {} 7 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/token/store/model/JsonWebToken.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.token.store.model; 2 | 3 | import javax.persistence.DiscriminatorValue; 4 | import javax.persistence.Entity; 5 | import javax.validation.constraints.NotNull; 6 | 7 | @Entity 8 | @DiscriminatorValue("jwt") 9 | public class JsonWebToken extends Token { 10 | 11 | @NotNull 12 | private boolean accessToken; 13 | 14 | public boolean isAccessToken() { 15 | return accessToken; 16 | } 17 | 18 | public void setAccessToken(boolean idToken) { 19 | this.accessToken = idToken; 20 | } 21 | 22 | @Override 23 | public boolean isReferenceToken() { 24 | return false; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return "JsonWebToken{" + 30 | "idToken=" + accessToken + 31 | "} " + super.toString(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/token/store/model/OpaqueToken.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.token.store.model; 2 | 3 | import org.springframework.security.authentication.BadCredentialsException; 4 | 5 | import javax.persistence.DiscriminatorValue; 6 | import javax.persistence.ElementCollection; 7 | import javax.persistence.Entity; 8 | import javax.persistence.FetchType; 9 | import javax.validation.constraints.NotBlank; 10 | import javax.validation.constraints.NotNull; 11 | import javax.validation.constraints.Size; 12 | import java.time.LocalDateTime; 13 | import java.util.Set; 14 | 15 | @Entity 16 | @DiscriminatorValue("opaque") 17 | public class OpaqueToken extends Token { 18 | 19 | @NotBlank 20 | @Size(max = 200) 21 | private String subject; 22 | 23 | @NotBlank 24 | @Size(max = 200) 25 | private String clientId; 26 | 27 | @NotBlank 28 | @Size(max = 200) 29 | private String issuer; 30 | 31 | @NotNull 32 | @ElementCollection(fetch = FetchType.EAGER) 33 | private Set scope; 34 | 35 | @NotNull 36 | private LocalDateTime issuedAt; 37 | 38 | @NotNull 39 | private LocalDateTime notBefore; 40 | 41 | @NotNull 42 | private boolean refreshToken; 43 | 44 | public String getSubject() { 45 | return subject; 46 | } 47 | 48 | public void setSubject(String subject) { 49 | this.subject = subject; 50 | } 51 | 52 | public String getClientId() { 53 | return clientId; 54 | } 55 | 56 | public void setClientId(String clientId) { 57 | this.clientId = clientId; 58 | } 59 | 60 | public String getIssuer() { 61 | return issuer; 62 | } 63 | 64 | public void setIssuer(String issuer) { 65 | this.issuer = issuer; 66 | } 67 | 68 | public Set getScope() { 69 | return scope; 70 | } 71 | 72 | public void setScope(Set scope) { 73 | this.scope = scope; 74 | } 75 | 76 | public LocalDateTime getIssuedAt() { 77 | return issuedAt; 78 | } 79 | 80 | public void setIssuedAt(LocalDateTime issuedAt) { 81 | this.issuedAt = issuedAt; 82 | } 83 | 84 | public LocalDateTime getNotBefore() { 85 | return notBefore; 86 | } 87 | 88 | public void setNotBefore(LocalDateTime notBefore) { 89 | this.notBefore = notBefore; 90 | } 91 | 92 | public boolean isRefreshToken() { 93 | return refreshToken; 94 | } 95 | 96 | public void setRefreshToken(boolean refreshToken) { 97 | this.refreshToken = refreshToken; 98 | } 99 | 100 | @Override 101 | public boolean isReferenceToken() { 102 | return true; 103 | } 104 | 105 | public void validate() { 106 | if (LocalDateTime.now().isAfter(this.getExpiry())) { 107 | throw new BadCredentialsException("Expired"); 108 | } 109 | if (LocalDateTime.now().isBefore(this.getNotBefore())) { 110 | throw new BadCredentialsException("Not yet valid"); 111 | } 112 | } 113 | 114 | @Override 115 | public String toString() { 116 | return "OpaqueToken{" + 117 | "subject='" + subject + '\'' + 118 | ", clientId='" + clientId + '\'' + 119 | ", issuer='" + issuer + '\'' + 120 | ", scope='" + getScope() + '\'' + 121 | ", issuedAt=" + issuedAt + 122 | ", notBefore=" + notBefore + 123 | ", refreshToken=" + refreshToken + 124 | "} " + super.toString(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/token/store/model/Token.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.token.store.model; 2 | 3 | import org.springframework.data.jpa.domain.AbstractPersistable; 4 | 5 | import javax.persistence.MappedSuperclass; 6 | import javax.validation.constraints.NotBlank; 7 | import javax.validation.constraints.NotNull; 8 | import javax.validation.constraints.Size; 9 | import java.time.LocalDateTime; 10 | 11 | @MappedSuperclass 12 | public abstract class Token extends AbstractPersistable { 13 | 14 | @NotBlank 15 | @Size(max = 2000) 16 | private String value; 17 | 18 | @NotNull private LocalDateTime expiry; 19 | 20 | private boolean revoked; 21 | 22 | public Token() {} 23 | 24 | public String getValue() { 25 | return value; 26 | } 27 | 28 | public void setValue(String value) { 29 | this.value = value; 30 | } 31 | 32 | public LocalDateTime getExpiry() { 33 | return expiry; 34 | } 35 | 36 | public void setExpiry(LocalDateTime expiry) { 37 | this.expiry = expiry; 38 | } 39 | 40 | public boolean isRevoked() { 41 | return revoked; 42 | } 43 | 44 | public void setRevoked(boolean revoked) { 45 | this.revoked = revoked; 46 | } 47 | 48 | public abstract boolean isReferenceToken(); 49 | 50 | @Override 51 | public String toString() { 52 | return "Token{" 53 | + "value='" 54 | + value 55 | + '\'' 56 | + ", expiry=" 57 | + expiry 58 | + ", revoked=" 59 | + revoked 60 | + ", referenceToken=" 61 | + isReferenceToken() 62 | + '}'; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/example/authorizationserver/web/MainWebController.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.web; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class MainWebController { 8 | 9 | @GetMapping("/") 10 | public String index() { 11 | return "index"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | open-in-view: false 4 | jackson: 5 | date-format: com.fasterxml.jackson.databind.util.StdDateFormat 6 | default-property-inclusion: non_null 7 | server: 8 | port: 9090 9 | servlet: 10 | context-path: /auth 11 | error: 12 | include-stacktrace: never 13 | 14 | auth-server: 15 | issuer: http://localhost:${server.port}${server.servlet.context-path} 16 | access-token: 17 | default-format: jwt 18 | lifetime: 10m 19 | id-token: 20 | lifetime: 5m 21 | refresh-token: 22 | lifetime: 8h 23 | max-lifetime: 8h 24 | 25 | logging: 26 | level: 27 | com: 28 | example: 29 | authorizationserver: trace 30 | org: 31 | springframework: 32 | security: debug -------------------------------------------------------------------------------- /src/main/resources/messages.properties: -------------------------------------------------------------------------------- 1 | client.list=Client Liste 2 | client.clientid=Client-Id 3 | client.clientsecret=Client-Secret 4 | client.confidential=Confidential 5 | client.granttypes=Unterstützte Grants 6 | client.accesstokenformat=Access-Token Format 7 | client.redirecturis=Redirect Uris 8 | client.corsuris=CORS Uris 9 | user.list=Benutzerliste 10 | user.username=Benutzername 11 | user.firstname=First name 12 | user.lastname=Last name 13 | user.email=Email 14 | admin.ui=Verwaltung 15 | admin.userlist=Benutzerliste 16 | admin.clientlist=Liste Registrierter Clients 17 | openid.config=OpenID Connect Discovery Information 18 | index.ui=Authorization Server 19 | user.groups=Gruppen -------------------------------------------------------------------------------- /src/main/resources/messages_de.properties: -------------------------------------------------------------------------------- 1 | client.list=Client Liste 2 | client.clientid=Client-Id 3 | client.clientsecret=Client-Secret 4 | client.confidential=Confidential 5 | client.granttypes=Unterstützte Grants 6 | client.accesstokenformat=Access-Token Format 7 | client.redirecturis=Redirect Uris 8 | client.corsuris=CORS Uris 9 | user.list=Benutzerliste 10 | user.username=Benutzername 11 | user.firstname=Vorname 12 | user.lastname=Nachname 13 | user.email=Email 14 | admin.ui=Verwaltung 15 | admin.userlist=Benutzerliste 16 | admin.clientlist=Liste Registrierter Clients 17 | openid.config=OpenID Connect Discovery Information 18 | index.ui=Authorization Server 19 | user.groups=Gruppen -------------------------------------------------------------------------------- /src/main/resources/messages_en.properties: -------------------------------------------------------------------------------- 1 | client.list=Client List 2 | client.clientid=Client-Id 3 | client.clientsecret=Client-Secret 4 | client.confidential=Confidential 5 | client.granttypes=Supported Grants 6 | client.accesstokenformat=Access-Token Format 7 | client.redirecturis=Redirect Uris 8 | client.corsuris=CORS Uris 9 | user.list=Users List 10 | user.username=Username 11 | user.firstname=First name 12 | user.lastname=Last name 13 | user.email=Email 14 | admin.ui=Administration 15 | admin.userlist=User List 16 | admin.clientlist=Registered Client List 17 | openid.config=OpenID Connect Discovery Information 18 | index.ui=Authorization Server 19 | user.groups=Groups -------------------------------------------------------------------------------- /src/main/resources/static/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /src/main/resources/templates/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Admin 6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 |

Admin

14 | 15 |
    16 |
  • 17 |
  • 18 |
  • 19 |
20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/resources/templates/clientlist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Userlist 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

List of Clients

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
Client-IdConfidentialSupported GrantsAccesstoken FormatRedirect-UrisCORS-Uris
38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/resources/templates/consent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Consent 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Consent

14 |

An OAuth 2.0 client is requesting your permission

15 | 16 |
    17 |
  • Scope
  • 18 |
19 | 20 |
21 |
22 | 23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 | -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Overview 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Overview

15 | 16 |
    17 |
  • 18 |
19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/templates/userlist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Userlist 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

List of Users

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
First nameFirst nameLast name
32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/test/java/com/example/authorizationserver/AuthorizationServerApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver; 2 | 3 | import com.example.authorizationserver.oauth.endpoint.AuthorizationEndpoint; 4 | import com.example.authorizationserver.oauth.endpoint.token.TokenEndpoint; 5 | import com.example.authorizationserver.oidc.endpoint.userinfo.UserInfoEndpoint; 6 | import org.junit.jupiter.api.DisplayName; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.test.annotation.DirtiesContext; 11 | import org.springframework.test.context.ActiveProfiles; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | @ActiveProfiles("integration-test") 16 | @DirtiesContext 17 | @SpringBootTest 18 | class AuthorizationServerApplicationTests { 19 | 20 | @Autowired 21 | private AuthorizationEndpoint authorizationEndpoint; 22 | 23 | @Autowired 24 | private TokenEndpoint tokenEndpoint; 25 | 26 | @Autowired 27 | private UserInfoEndpoint userInfoEndpoint; 28 | 29 | @Test 30 | @DisplayName("Verify that the authorization server application starts") 31 | void verifyApplicationStart() { 32 | 33 | assertThat(authorizationEndpoint).isNotNull(); 34 | assertThat(tokenEndpoint).isNotNull(); 35 | assertThat(userInfoEndpoint).isNotNull(); 36 | 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/example/authorizationserver/annotation/WebIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.annotation; 2 | 3 | import org.springframework.boot.test.context.SpringBootTest; 4 | import org.springframework.test.annotation.DirtiesContext; 5 | import org.springframework.test.context.ActiveProfiles; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.ElementType; 9 | import java.lang.annotation.Inherited; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | import java.lang.annotation.Target; 13 | 14 | @Target(ElementType.TYPE) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | @Documented 17 | @Inherited 18 | @ActiveProfiles("integration-test") 19 | @DirtiesContext 20 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 21 | public @interface WebIntegrationTest { 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/example/authorizationserver/jwks/JwksEndpointIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.jwks; 2 | 3 | import com.example.authorizationserver.annotation.WebIntegrationTest; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.test.web.servlet.MockMvc; 10 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 11 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 12 | import org.springframework.web.context.WebApplicationContext; 13 | 14 | import static org.hamcrest.Matchers.equalTo; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 16 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 17 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 18 | 19 | @WebIntegrationTest 20 | class JwksEndpointIntegrationTest { 21 | 22 | @Autowired private WebApplicationContext webApplicationContext; 23 | 24 | private MockMvc mockMvc; 25 | 26 | @BeforeEach 27 | void initMockMvc() { 28 | this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 29 | } 30 | 31 | @DisplayName("JWKS endpoint is accessible and returns expected result") 32 | @Test 33 | void jwksEndpoint() throws Exception { 34 | this.mockMvc 35 | .perform( 36 | MockMvcRequestBuilders.get(JwksEndpoint.ENDPOINT).accept(MediaType.APPLICATION_JSON)) 37 | .andDo(print()) 38 | .andExpect(status().isOk()) 39 | .andExpect(jsonPath("$.keys.length()", equalTo(1))) 40 | .andExpect(jsonPath("$.keys[0].kty", equalTo("RSA"))); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/example/authorizationserver/oidc/endpoint/DiscoveryEndpointIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oidc.endpoint; 2 | 3 | import com.example.authorizationserver.annotation.WebIntegrationTest; 4 | import com.example.authorizationserver.oauth.endpoint.AuthorizationEndpoint; 5 | import com.example.authorizationserver.oidc.endpoint.discovery.Discovery; 6 | import com.example.authorizationserver.oidc.endpoint.discovery.DiscoveryEndpoint; 7 | import io.restassured.http.ContentType; 8 | import io.restassured.module.mockmvc.RestAssuredMockMvc; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.web.context.WebApplicationContext; 13 | 14 | import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.hamcrest.Matchers.empty; 17 | import static org.hamcrest.Matchers.not; 18 | 19 | @WebIntegrationTest 20 | class DiscoveryEndpointIntegrationTest { 21 | 22 | @Autowired private WebApplicationContext webApplicationContext; 23 | 24 | @BeforeEach 25 | void initMockMvc() { 26 | RestAssuredMockMvc.webAppContextSetup(webApplicationContext); 27 | } 28 | 29 | @Test 30 | void discoveryEndpoint() { 31 | Discovery discovery = 32 | given() 33 | .when() 34 | .get(DiscoveryEndpoint.ENDPOINT) 35 | .then() 36 | .log() 37 | .ifValidationFails() 38 | .statusCode(200) 39 | .contentType(ContentType.JSON) 40 | .body(not(empty())) 41 | .extract() 42 | .as(Discovery.class); 43 | assertThat(discovery).isNotNull(); 44 | assertThat(discovery.getAuthorization_endpoint()) 45 | .isEqualTo("http://localhost:8080/auth" + AuthorizationEndpoint.ENDPOINT); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/example/authorizationserver/oidc/endpoint/UserInfoEndpointIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.oidc.endpoint; 2 | 3 | import com.example.authorizationserver.annotation.WebIntegrationTest; 4 | import com.example.authorizationserver.oidc.endpoint.userinfo.UserInfo; 5 | import com.example.authorizationserver.scim.model.ScimUserEntity; 6 | import com.example.authorizationserver.scim.service.ScimService; 7 | import com.example.authorizationserver.token.store.TokenService; 8 | import com.example.authorizationserver.token.store.model.JsonWebToken; 9 | import com.example.authorizationserver.token.store.model.OpaqueToken; 10 | import com.nimbusds.jose.JOSEException; 11 | import io.restassured.http.ContentType; 12 | import io.restassured.module.mockmvc.RestAssuredMockMvc; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.web.context.WebApplicationContext; 17 | 18 | import java.time.Duration; 19 | import java.util.Collections; 20 | import java.util.Optional; 21 | 22 | import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | import static org.hamcrest.Matchers.empty; 25 | import static org.hamcrest.Matchers.not; 26 | 27 | @WebIntegrationTest 28 | class UserInfoEndpointIntegrationTest { 29 | 30 | @Autowired 31 | private TokenService tokenService; 32 | @Autowired 33 | private ScimService scimService; 34 | @Autowired 35 | private WebApplicationContext webApplicationContext; 36 | private ScimUserEntity bwayne_user; 37 | 38 | @BeforeEach 39 | void initMockMvc() { 40 | RestAssuredMockMvc.webAppContextSetup(webApplicationContext); 41 | Optional bwayne = scimService.findUserByUserName("bwayne"); 42 | bwayne.ifPresent(user -> bwayne_user = user); 43 | } 44 | 45 | @Test 46 | void userInfoWithJwtToken() throws JOSEException { 47 | JsonWebToken jsonWebToken = 48 | tokenService.createPersonalizedJwtAccessToken( 49 | bwayne_user, "confidential-demo", "nonce", Collections.singleton("OPENID"), Duration.ofMinutes(5)); 50 | UserInfo userInfo = 51 | given() 52 | .header("Authorization", "Bearer " + jsonWebToken.getValue()) 53 | .when() 54 | .get("/userinfo") 55 | .then() 56 | .log() 57 | .ifValidationFails() 58 | .statusCode(200) 59 | .contentType(ContentType.JSON) 60 | .body(not(empty())) 61 | .extract() 62 | .as(UserInfo.class); 63 | assertThat(userInfo).isNotNull(); 64 | assertThat(userInfo.getName()).isEqualTo("bwayne"); 65 | } 66 | 67 | @Test 68 | void userInfoWithOpaqueToken() { 69 | 70 | OpaqueToken opaqueToken = 71 | tokenService.createPersonalizedOpaqueAccessToken( 72 | bwayne_user, "confidential-demo", Collections.singleton("OPENID"), Duration.ofMinutes(5)); 73 | 74 | UserInfo userInfo = 75 | given() 76 | .header("Authorization", "Bearer " + opaqueToken.getValue()) 77 | .when() 78 | .get("/userinfo") 79 | .then() 80 | .log() 81 | .ifValidationFails() 82 | .statusCode(200) 83 | .contentType(ContentType.JSON) 84 | .body(not(empty())) 85 | .extract() 86 | .as(UserInfo.class); 87 | assertThat(userInfo).isNotNull(); 88 | assertThat(userInfo.getName()).isEqualTo("bwayne"); 89 | } 90 | 91 | @Test 92 | void userInfoWithInvalidToken() { 93 | 94 | UserInfo userInfo = 95 | given() 96 | .header("Authorization", "Bearer 12345") 97 | .when() 98 | .get("/userinfo") 99 | .then() 100 | .log() 101 | .ifValidationFails() 102 | .statusCode(401) 103 | .contentType(ContentType.JSON) 104 | .body(not(empty())) 105 | .extract() 106 | .as(UserInfo.class); 107 | assertThat(userInfo).isNotNull(); 108 | assertThat(userInfo.getError()).isEqualTo("invalid_token"); 109 | assertThat(userInfo.getError_description()).isEqualTo("Access Token is invalid"); 110 | } 111 | 112 | @Test 113 | void userInfoWithMissingToken() { 114 | 115 | UserInfo userInfo = 116 | given() 117 | .when() 118 | .get("/userinfo") 119 | .then() 120 | .log() 121 | .ifValidationFails() 122 | .statusCode(401) 123 | .contentType(ContentType.JSON) 124 | .body(not(empty())) 125 | .extract() 126 | .as(UserInfo.class); 127 | assertThat(userInfo).isNotNull(); 128 | assertThat(userInfo.getError()).isEqualTo("invalid_token"); 129 | assertThat(userInfo.getError_description()).isEqualTo("Access Token is required"); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/test/java/com/example/authorizationserver/token/jwt/JsonWebTokenServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.example.authorizationserver.token.jwt; 2 | 3 | import com.example.authorizationserver.jwks.JwtPki; 4 | import com.example.authorizationserver.scim.model.*; 5 | import com.nimbusds.jose.JOSEException; 6 | import com.nimbusds.jwt.JWTClaimsSet; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.util.IdGenerator; 11 | 12 | import java.text.ParseException; 13 | import java.time.LocalDateTime; 14 | import java.util.Collections; 15 | import java.util.List; 16 | import java.util.Set; 17 | import java.util.UUID; 18 | 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | 21 | @SpringBootTest 22 | class JsonWebTokenServiceTest { 23 | 24 | private final JsonWebTokenService cut; 25 | 26 | JsonWebTokenServiceTest(@Autowired JwtPki jwtPki, @Autowired IdGenerator idGenerator) { 27 | this.cut = new JsonWebTokenService(jwtPki, idGenerator); 28 | } 29 | 30 | @Test 31 | void createPersonalizedToken() throws JOSEException, ParseException { 32 | String personalizedToken = cut.createPersonalizedToken(true, "myclient", List.of("myaudience"), 33 | Collections.singleton("openid"), new ScimUserEntity(UUID.randomUUID(), "1234", "fname", "First", "Name", true, 34 | "secret", Set.of(new ScimEmailEntity("first.name@example.com", "work", true)), 35 | Set.of(new ScimPhoneNumberEntity("12345", "work")), Set.of(new ScimImsEntity("12345", "work")), 36 | Set.of(new ScimAddressEntity("street", "locality", "region", "12345", "country", "work", true)), 37 | Set.of(new ScimUserGroupEntity( 38 | new ScimUserEntity(UUID.randomUUID(), "username", 39 | "family", "given", true, "secret", null, null, Set.of("USER")), 40 | new ScimGroupEntity(UUID.randomUUID(), "12345", "test_group", null))), 41 | Set.of("entitlement"), Set.of("USER")), "nonce", LocalDateTime.now().plusMinutes(5)); 42 | JWTClaimsSet parsedToken = cut.parseAndValidateToken(personalizedToken); 43 | assertThat(parsedToken).isNotNull(); 44 | } 45 | 46 | @Test 47 | void createPersonalizedTokenWithAllScopes() throws JOSEException, ParseException { 48 | String personalizedToken = cut.createPersonalizedToken(true, "myclient", List.of("myaudience"), 49 | Set.of("openid", "profile", "email", "phone", "address"), new ScimUserEntity(UUID.randomUUID(), "1234", "fname", "First", "Name", true, 50 | "secret", Set.of(new ScimEmailEntity("first.name@example.com", "work", true)), 51 | Set.of(new ScimPhoneNumberEntity("12345", "work")), Set.of(new ScimImsEntity("12345", "work")), 52 | Set.of(new ScimAddressEntity("street", "locality", "region", "12345", "country", "work", true)), 53 | Set.of(new ScimUserGroupEntity( 54 | new ScimUserEntity(UUID.randomUUID(), "username", 55 | "family", "given", true, "secret", null, null, Set.of("USER")), 56 | new ScimGroupEntity(UUID.randomUUID(), "12345", "test_group", null))), 57 | Set.of("entitlement"), Set.of("USER")), "nonce", LocalDateTime.now().plusMinutes(5)); 58 | JWTClaimsSet parsedToken = cut.parseAndValidateToken(personalizedToken); 59 | assertThat(parsedToken).isNotNull(); 60 | } 61 | 62 | @Test 63 | void createAnonymousToken() throws JOSEException, ParseException { 64 | String anonymousToken = cut.createAnonymousToken(true, "myclient", List.of("myaudience"), 65 | Collections.singleton("openid"), LocalDateTime.now().plusMinutes(5)); 66 | JWTClaimsSet parsedToken = cut.parseAndValidateToken(anonymousToken); 67 | assertThat(parsedToken).isNotNull(); 68 | } 69 | } -------------------------------------------------------------------------------- /src/test/resources/application-integration-test.yml: -------------------------------------------------------------------------------- 1 | server: 2 | error: 3 | include-stacktrace: always 4 | 5 | auth-server: 6 | issuer: http://localhost:8080${server.servlet.context-path} 7 | 8 | logging: 9 | level: 10 | com: 11 | example: 12 | authorizationserver: trace 13 | org: 14 | springframework: 15 | security: info 16 | 17 | spring: 18 | jpa: 19 | hibernate: 20 | ddl-auto: update 21 | --------------------------------------------------------------------------------