{"id":20673231,"url":"https://github.com/coder966/spring-boot-starter-multi-security-realms","last_synced_at":"2025-03-10T17:59:46.615Z","repository":{"id":213858989,"uuid":"732492621","full_name":"coder966/spring-boot-starter-multi-security-realms","owner":"coder966","description":"Support multiple security realms in a single Spring Boot application","archived":false,"fork":false,"pushed_at":"2024-09-13T09:46:19.000Z","size":945,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-17T14:18:32.790Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/coder966.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-12-16T21:15:38.000Z","updated_at":"2024-09-13T09:46:23.000Z","dependencies_parsed_at":"2023-12-30T00:33:32.839Z","dependency_job_id":"bf32764d-f6e4-4c7f-a7ba-adf63397003c","html_url":"https://github.com/coder966/spring-boot-starter-multi-security-realms","commit_stats":null,"previous_names":["coder966/spring-boot-starter-multi-security-realms"],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coder966%2Fspring-boot-starter-multi-security-realms","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coder966%2Fspring-boot-starter-multi-security-realms/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coder966%2Fspring-boot-starter-multi-security-realms/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coder966%2Fspring-boot-starter-multi-security-realms/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/coder966","download_url":"https://codeload.github.com/coder966/spring-boot-starter-multi-security-realms/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242900001,"owners_count":20203704,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-16T20:40:34.361Z","updated_at":"2025-03-10T17:59:46.584Z","avatar_url":"https://github.com/coder966.png","language":"Java","readme":"# Spring Multi Security Realms\n\n[![Maven Central](https://img.shields.io/maven-central/v/net.coder966.spring/spring-boot-starter-multi-security-realms)](https://central.sonatype.com/artifact/net.coder966.spring/spring-boot-starter-multi-security-realms)\n\nSupport multiple security realms in a single Spring Boot application.\n\n## What is a Security Realm\n\nA realm is a scope of operations. A security realm is a security scope which defines protected resources and users in that realm.\n\nFor example, suppose you have a multi-tenant online e-store application. This application probably support these types of users:\n\n- admin users / helpdesk users (realm)\n- store owner users (realm)\n- store customer users (realm)\n\nThese different user types are probably authenticated (login mechanism/flow/steps) and authorized (protected APIs) differently.\nConfiguring this in Spring can be tricky and a bit complicated. You can even potentially introduce security bugs if you try to implement these features\nmanually.\n\n## Why `spring-boot-starter-multi-security-realms`\n\nThis library allows you to easily and declaratively define these realms. It also brings extra features like:\n\n- Multi-steps authentication support (aka Multi-Factor Authentication MFA). For example: username \u0026 password step, then OTP step, etc... You don't have to think\n  about how to implement this, just use the built-in support.\n- Ability to define public apis per realm without the need to access and update the `SecurityFilterChain` manually.\n  This is helpful if your application is huge, and you want to define public apis in segregated modules without the need to define them in a central place.\n- You still have full control and can define custom `SecurityFilterChain`s if you wish. By default,\n  this library creates a default `SecurityFilterChain` and injects the multi realm support into it.\n\n## Usage\n\n### Requirements\n\n- Spring Boot \u003e= 3.x.x\n\n### Installation\n\nMaven:\n\n```xml\n\n\u003cdependency\u003e\n    \u003cgroupId\u003enet.coder966.spring\u003c/groupId\u003e\n    \u003cartifactId\u003espring-boot-starter-multi-security-realms\u003c/artifactId\u003e\n    \u003cversion\u003e0.1.2\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\nGradle:\n\n```groovy\nimplementation 'net.coder966.spring:spring-boot-starter-multi-security-realms:0.1.2'\n```\n\n## Usage\n\n### Define security realms\n\nTo define a realm, simply create a Spring component and annotate it with `@SecurityRealm`.\nHere in this example, we define two realms (normal-user \u0026 admin-user).\n\n#### NormalUserSecurityRealm.java\n```java\n@Slf4j\n@SecurityRealm(\n        name = \"NORMAL_USER\",\n        authenticationEndpoint = \"/normal-user/auth\",\n        firstStepName = StepNames.USERNAME_AND_PASSWORD,\n        publicApis = {\n                \"/my-third-open-api\",\n                \"/my-forth-open-api\"\n        }\n)\npublic class NormalUserSecurityRealm {\n\n    @Autowired\n    private NormalUserRepo normalUserRepo;\n\n    @Transactional\n    @AuthenticationStep(StepNames.USERNAME_AND_PASSWORD)\n    public SecurityRealmAuthentication firstAuthenticationStep(@RequestBody AuthUsernameAndPasswordStepRequest request) {\n        Optional\u003cNormalUser\u003e optionalUser = normalUserRepo.findByUsername(request.getUsername());\n        if (optionalUser.isEmpty()) {\n            throw new SecurityRealmAuthenticationException(ErrorCodes.BAD_CREDENTIALS);\n        }\n        NormalUser user = optionalUser.get();\n\n        // WARNING: FOR DEMO PURPOSE ONLY\n        if (!user.getPassword().equals(request.getPassword())) {\n            throw new SecurityRealmAuthenticationException(ErrorCodes.BAD_CREDENTIALS);\n        }\n\n        // TODO: send otp to mobile\n        String otp = \"1234\"; // random\n        user.setOtp(otp);\n        user = normalUserRepo.save(user);\n\n        // here we specify the next step name in the SecurityRealmAuthentication\n        // if this is the last step, then don't specify the next step name, or send null\n        return new SecurityRealmAuthentication(user.getUsername(), null, StepNames.OTP);\n    }\n\n    @Transactional\n    @AuthenticationStep(StepNames.OTP)\n    public SecurityRealmAuthentication otpAuthenticationStep(@RequestBody AuthOtpStepRequest request) {\n        SecurityRealmAuthentication previousStepAuth = (SecurityRealmAuthentication) SecurityContextHolder.getContext().getAuthentication();\n\n        String otp = request.getOtp();\n\n        NormalUser user = normalUserRepo.findByUsername(previousStepAuth.getName()).get();\n\n        if (!user.getOtp().equals(otp)) {\n            throw new SecurityRealmAuthenticationException(ErrorCodes.BAD_OTP);\n        }\n\n        // clear otp\n        user.setOtp(otp);\n        user = normalUserRepo.save(user);\n\n        return new SecurityRealmAuthentication(user.getUsername(), null);\n    }\n}\n```\n\n#### AdminUserSecurityRealm.java\n\n```java\n@Slf4j\n@SecurityRealm(\n        name = \"ADMIN_USER\",\n        authenticationEndpoint = \"/admin-user/auth\",\n        firstStepName = StepNames.USERNAME_AND_PASSWORD,\n        publicApis = {\n                \"/my-first-open-api\",\n                \"/my-second-open-api\"\n        }\n)\npublic class AdminUserSecurityRealm {\n\n    @Autowired\n    private AdminUserRepo adminUserRepo;\n\n    @Transactional\n    @AuthenticationStep(StepNames.USERNAME_AND_PASSWORD)\n    public SecurityRealmAuthentication firstAuthenticationStep(@RequestBody AuthUsernameAndPasswordStepRequest request) {\n        Optional\u003cAdminUser\u003e optionalUser = adminUserRepo.findByUsername(request.getUsername());\n        if (optionalUser.isEmpty()) {\n            throw new SecurityRealmAuthenticationException(ErrorCodes.BAD_CREDENTIALS);\n        }\n        AdminUser user = optionalUser.get();\n\n        // WARNING: FOR DEMO PURPOSE ONLY\n        if (!user.getPassword().equals(request.getPassword())) {\n            throw new SecurityRealmAuthenticationException(ErrorCodes.BAD_CREDENTIALS);\n        }\n\n        // TODO: send otp to mobile\n        String otp = \"1234\"; // random\n        user.setOtp(otp);\n        user = adminUserRepo.save(user);\n\n        // here we specify the next step name in the SecurityRealmAuthentication\n        // if this is the last step, then don't specify the next step name, or send null\n        return new SecurityRealmAuthentication(user.getUsername(), null, StepNames.OTP);\n    }\n\n    @Transactional\n    @AuthenticationStep(StepNames.OTP)\n    public SecurityRealmAuthentication otpAuthenticationStep(@RequestBody AuthOtpStepRequest request) {\n        SecurityRealmAuthentication previousStepAuth = (SecurityRealmAuthentication) SecurityContextHolder.getContext().getAuthentication();\n\n        String otp = request.getOtp();\n\n        AdminUser user = adminUserRepo.findByUsername(previousStepAuth.getName()).get();\n\n        if (!user.getOtp().equals(otp)) {\n            throw new SecurityRealmAuthenticationException(ErrorCodes.BAD_OTP);\n        }\n\n        // clear otp\n        user.setOtp(otp);\n        user = adminUserRepo.save(user);\n\n        return new SecurityRealmAuthentication(user.getUsername(), null);\n    }\n}\n```\n\n### Client Application (Frontend)\n\n- The client app should call the realm authentication endpoint.\n- You will receive a JWT token in the response body as a string.\n- Store this token and pass in all subsequent requests in the `Authorization` header.\n- If the realm requires additional authentication steps from you (MFA),\n  you will see the required authentication step name in the response body `nextAuthenticationStep`. Render this step form and again submit to the same\n  authentication endpoint.\n- In any case, if there is an error in the authentication (for example, bad credentials), you will receive the error in the response body `error`.\n\n\n### Realm Protected APIs\n\nTo protect an api so that it can only be used by a certain realm users, you can use `@PreAuthorize(\"permitRealm('\u003crealm-role-name\u003e')\")`.\n\nExample:\n\n```java\n// adding this here, will apply it for all the endpoints in this controller\n@PreAuthorize(\"permitRealm('ADMIN_USER')\")\n@RestController\npublic class AdminUserController {\n\n    // OR it can be defined here at the method level\n    @PreAuthorize(\"permitRealm('ADMIN_USER')\")\n    @GetMapping(\"/admin-user/my-name\")\n    public String myName() {\n        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();\n        return authentication.getName(); // username\n    }\n\n}\n```\n\n## Tips\n\n### I want to define my own `SecurityFilterChain`\n\nIf you want to define a custom `SecurityFilterChain` then you need to add this filter `MultiSecurityRealmAuthenticationFilter`\nbefore `AnonymousAuthenticationFilter`.\n\n```java\n\n@Slf4j\n@Configuration\n@EnableMethodSecurity\n@RequiredArgsConstructor\npublic class SecurityConfig {\n\n    @Bean\n    protected SecurityFilterChain globalSecurityFilterChain(\n            HttpSecurity http,\n            MultiSecurityRealmAuthenticationFilter multiSecurityRealmAuthenticationFilter // inject this filter\n    ) throws Exception {\n\n        // this is optional. If you don't have a custom SecurityFilterChain then you don't need to do all of this\n        // A default SecurityFilterChain is configured out of the box.\n\n        // add it before AnonymousAuthenticationFilter\n        http.addFilterBefore(multiSecurityRealmAuthenticationFilter, AnonymousAuthenticationFilter.class);\n\n\n        // session should be disabled\n        http.sessionManagement(configurer -\u003e configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));\n\n\n        // disable csrf, because JWT token is not stored in the cookies, so CSRF protection is not needed\n        // you can still enable it, but you have to support it in your client application (frontend)\n        http.csrf(AbstractHttpConfigurer::disable);\n\n\n        // the reset of your configuration ....\n\n        return http.build();\n    }\n\n}\n```\n\n## License\n\n```txt\nCopyright 2023 Khalid H. Alharisi\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoder966%2Fspring-boot-starter-multi-security-realms","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcoder966%2Fspring-boot-starter-multi-security-realms","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoder966%2Fspring-boot-starter-multi-security-realms/lists"}