{"id":19656246,"url":"https://github.com/daggerok/spring-jwt-secured-apps","last_synced_at":"2025-02-27T02:19:28.003Z","repository":{"id":151042110,"uuid":"256638717","full_name":"daggerok/spring-jwt-secured-apps","owner":"daggerok","description":"From zero to JWT hero in Spring Boot Servlet app","archived":false,"fork":false,"pushed_at":"2020-04-19T17:26:35.000Z","size":108,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-10T00:41:50.053Z","etag":null,"topics":["github-actions-docker","github-actions-java","github-actions-javascript","jwt","jwt-auth","jwt-authentication","jwt-bearer-tokens","jwt-server","jwt-token","jwt-tokens","jwtauth","maven-release-plugin","release","release-automation","released","releases","spring-security","spring-security-jwt","spring-security-web","versions-maven-plugin"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/daggerok.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":"2020-04-18T00:33:34.000Z","updated_at":"2023-02-07T12:57:43.000Z","dependencies_parsed_at":null,"dependency_job_id":"f63a73b9-1f51-4485-9f2e-c8b522253352","html_url":"https://github.com/daggerok/spring-jwt-secured-apps","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daggerok%2Fspring-jwt-secured-apps","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daggerok%2Fspring-jwt-secured-apps/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daggerok%2Fspring-jwt-secured-apps/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daggerok%2Fspring-jwt-secured-apps/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/daggerok","download_url":"https://codeload.github.com/daggerok/spring-jwt-secured-apps/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240964607,"owners_count":19885771,"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":["github-actions-docker","github-actions-java","github-actions-javascript","jwt","jwt-auth","jwt-authentication","jwt-bearer-tokens","jwt-server","jwt-token","jwt-tokens","jwtauth","maven-release-plugin","release","release-automation","released","releases","spring-security","spring-security-jwt","spring-security-web","versions-maven-plugin"],"created_at":"2024-11-11T15:27:09.363Z","updated_at":"2025-02-27T02:19:27.978Z","avatar_url":"https://github.com/daggerok.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# spring-jwt-secured-apps [![CI](https://github.com/daggerok/spring-jwt-secured-apps/workflows/CI/badge.svg)](https://github.com/daggerok/spring-jwt-secured-apps/actions?query=workflow%3ACI)\nFrom zero to JWT hero in Spring Servlet applications!\n\n## Table of Content\n* [Step 0: No security](#step-0)\n* [Step 1: Spring Security defaults](#step-1)\n* [Step 2: Using custom WebSecurityConfigurerAdapter, UserDetailsService](#step-2)\n* [Step 3: Simple JWT integration](#step-3)\n* [Step 4: Teach Spring auth with JWT from request headers](#step-4)\n* [Step 5: Make application stateless](#step-5)\n* [Versioning and releasing](#maven)\n* [Resources and used links](#resources)\n\n## step: 0\n\nlet's use simple spring boot web app with `pom.xml` file:\n\n```xml\n  \u003cdependencies\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n      \u003cartifactId\u003espring-boot-starter-web\u003c/artifactId\u003e\n    \u003c/dependency\u003e\n  \u003c/dependencies\u003e\n```\n\nwith `SpringJwtSecuredAppsApplication.java` file:\n\n```java\n@Controller\nclass IndexPage {\n\n  @GetMapping(\"\")\n  String index() {\n    return \"index.html\";\n  }\n}\n\n@RestController\nclass HelloResource {\n\n  @GetMapping(\"/api/hello\")\n  Map\u003cString, String\u003e hello() {\n    return Map.of(\"Hello\", \"world\");\n  }\n}\n```\n\nwith `src/main/resources/static/index.html` file:\n\n```html\n\u003c!doctype html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003ctitle\u003eJWT\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003ch1\u003eHello\u003c/h1\u003e\n\u003cul id=\"app\"\u003e\u003c/ul\u003e\n\u003cscript\u003e\n  document.addEventListener('DOMContentLoaded', onDOMContentLoaded, false);\n\n  function onDOMContentLoaded() {\n    const headers = { 'Content-Type': 'application/json' };\n\n    let options = { method: 'GET', headers, };\n    fetch('/api/hello', options)\n      .then(response =\u003e response.json())\n      .then(json =\u003e {\n        console.log('json', json);\n        const textNode = document.createTextNode(JSON.stringify(json));\n        document.querySelector('#app').prepend(textNode);\n      })\n    ;\n  }\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\nwith that we can query with no security at all:\n\n```bash\nhttp :8080\nhttp :8080/api/hello\n```\n\n## step: 1\n\nlet's use default spring-security:\n\n```xml\n  \u003cdependencies\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n      \u003cartifactId\u003espring-boot-starter-security\u003c/artifactId\u003e\n    \u003c/dependency\u003e\n  \u003c/dependencies\u003e\n```\n\nuser has generated password (initially taken from server logs), so let's configure it in `application.properties` file:\n\n```properties\nspring.security.user.password=80427fb5-888f-4669-83c0-893ca655a82e\n```\n\nwith that we can query like so:\n\n```bash\nhttp -a user:80427fb5-888f-4669-83c0-893ca655a82e :8080\nhttp -a user:80427fb5-888f-4669-83c0-893ca655a82e :8080/api/hello\n```\n\n## step: 2\n\ncreate custom security config:\n\n```java\n@Configuration\n@RequiredArgsConstructor\nclass MyWebSecurity extends WebSecurityConfigurerAdapter {\n\n  final MyUserDetailsService myUserDetailsService;\n\n  @Override\n  protected void configure(AuthenticationManagerBuilder auth) throws Exception {\n    auth.userDetailsService(myUserDetailsService);\n  }\n}\n```\n\nwhere `UserDetailsService` implemented as follows:\n\n```java\n@Service\n@RequiredArgsConstructor\nclass MyUserDetailsService implements UserDetailsService {\n\n  final PasswordEncoder passwordEncoder;\n\n  @Override\n  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {\n    return Optional.ofNullable(username)\n                   .filter(u -\u003e u.contains(\"max\") || u.contains(\"dag\"))\n                   .map(u -\u003e new User(username,\n                                      passwordEncoder.encode(username),\n                                      AuthorityUtils.createAuthorityList(\"USER\")))\n                   .orElseThrow(() -\u003e new UsernameNotFoundException(String.format(\"User %s not found.\", username)));\n  }\n}\n```\n\nalso, we need `PasswordEncoder` in context:\n\n```java\n@Configuration\nclass MyPasswordEncoderConfig {\n\n  @Bean\n  PasswordEncoder passwordEncoder() {\n    return PasswordEncoderFactories.createDelegatingPasswordEncoder();\n  }\n}\n```\n\nwith that, we can use username and password, which must be the same and must contain `max` or `dag` words:\n\n```bash\nhttp -a max:max get :8080\nhttp -a daggerok:daggerok get :8080/api/hello\n```\n\n## step: 3\n\nfirst, let's add required dependencies:\n\n```xml\n  \u003cdependencies\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eio.jsonwebtoken\u003c/groupId\u003e\n      \u003cartifactId\u003ejjwt\u003c/artifactId\u003e\n    \u003c/dependency\u003e\n  \u003c/dependencies\u003e\n```\n\n### update backend\n\nimplement auth rest resources:\n\n```java\n@RestController\n@RequiredArgsConstructor\nclass JwtResource {\n\n  final JwtService jwtService;\n  final UserDetailsService userDetailsService;\n  final AuthenticationManager authenticationManager;\n\n  @PostMapping(\"/api/auth\")\n  AuthenticationResponse authenticate(@RequestBody AuthenticationRequest request) {\n    var token = new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword());\n    var authentication = authenticationManager.authenticate(token);\n    var userDetails = userDetailsService.loadUserByUsername(request.getUsername());\n    var jwtToken = jwtService.generateToken(userDetails);\n    return new AuthenticationResponse(jwtToken);\n  }\n}\n```\n\nwhere:\n\n_JwtService_\n\n```java\n@Service\nclass JwtService {\n  String generateToken(UserDetails userDetails) {\n    /* Skipped jwt infrastructure logic... See sources for details */\n  }\n}\n```\n\n_AuthenticationManager_\n\n```java\nclass MyWebSecurity extends WebSecurityConfigurerAdapter {\n  \n  @Override\n  @Bean // Requires to being able to inject AuthenticationManager bean in our AuthResource.\n  public AuthenticationManager authenticationManagerBean() throws Exception {\n    return super.authenticationManagerBean();\n  }\n\n  /**\n   * Requires to:\n   * - post authentication without CSRF protection\n   * - permit all requests for index page and /api/auth auth resource path\n   */\n  @Override\n  protected void configure(HttpSecurity http) throws Exception {\n    http.authorizeRequests()\n          .mvcMatchers(HttpMethod.GET, \"/\").permitAll()\n          .mvcMatchers(HttpMethod.POST, \"/api/auth\").permitAll()\n          .anyRequest().fullyAuthenticated()//.authenticated()//\n        .and()\n          .csrf().disable()\n        // .formLogin()\n    ;\n  }\n\n  // ...\n}\n```\n\n### update frontend\n\n```js\n  options = {\n    method: 'POST', headers,\n    body: JSON.stringify({ username: 'dag', password: 'dag' }),\n  };\n\n  fetch('/api/auth', options)\n    .catch(errorHandler)\n    .then(response =\u003e response.json())\n    .then(json =\u003e {\n      console.log('auth json', json);\n      const result = JSON.stringify(json);\n      const textNode = document.createTextNode(result);\n      document.querySelector('#app').prepend(textNode);\n    })\n  ;\n\n  function errorHandler(reason) {\n    console.log(reason);\n  }\n```\n\n### test\n\nwith that, open http://127.0.0.1:8080 page, or use username and password, which must be the same and must contain\n`max` or `dag` words in your _AuthenticationRequest_:\n\n```bash\nhttp post :8080/api/auth username=dag password=dag\n```\n\n## step: 4\n\nlet's now implement request filter interceptor, which is going to\nparse authorization header for Bearer token and authorizing spring\nsecurity context accordingly to its validity:\n\n_JwtRequestFilter_\n\n```java\n@Component\n@RequiredArgsConstructor\nclass JwtRequestFilter extends OncePerRequestFilter {\n\n  final JwtService jwtService;\n  final UserDetailsService userDetailsService;\n\n  @Override\n  protected void doFilterInternal(HttpServletRequest httpServletRequest,\n                                  HttpServletResponse httpServletResponse,\n                                  FilterChain filterChain) throws ServletException, IOException {\n\n    var prefix = \"Bearer \";\n    var authorizationHeader = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION);\n\n    Optional.ofNullable(authorizationHeader).ifPresent(ah -\u003e {\n\n      var parts = ah.split(prefix);\n      if (parts.length \u003c 2) return;\n\n      var accessToken = parts[1].trim();\n      Optional.of(accessToken). filter(Predicate.not(String::isBlank)).ifPresent(at -\u003e {\n\n        if (jwtService.isTokenExpire(at)) return;\n\n        var username = jwtService.extractUsername(at);\n        var userDetails = userDetailsService.loadUserByUsername(username);\n        if (!jwtService.validateToken(at, userDetails)) return;\n\n        var authentication = new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities());\n        var details = new WebAuthenticationDetailsSource().buildDetails(httpServletRequest);\n\n        authentication.setDetails(details);\n        SecurityContextHolder.getContext().setAuthentication(authentication);\n      });\n    });\n\n    filterChain.doFilter(httpServletRequest, httpServletResponse);\n  }\n}\n```\n\nfinally, update fronted to leverage localStorage as\naccessToken store:\n\n```js\nfunction headersWithAuth() {\n  const accessToken = localStorage.getItem('accessToken');\n  return !accessToken ? headers : Object.assign({}, headers,\n    { Authorization: 'Bearer ' + accessToken });\n}\n\nfunction auth() {\n  const options = {\n    method: 'POST', headers: headersWithAuth(),\n    body: JSON.stringify({ username: 'max', password: 'max' }),\n  };\n  fetch('/api/auth', options)\n    .then(response =\u003e response.json())\n    .then(json =\u003e {\n      if (json.accessToken) localStorage.setItem('accessToken', json.accessToken);\n    })\n  ;\n}\n\nfunction api() {\n  const options = { method: 'GET', headers: headersWithAuth() };\n  fetch('/api/hello', options)\n    .then(response =\u003e response.json())\n    .then(json =\u003e {\n      if (json.status \u0026\u0026 json.status \u003e= 400) {\n        auth();\n        return;\n      }\n      const result = JSON.stringify(json);\n      const textNode = document.createTextNode(result);\n      const div = document.createElement('div');\n      div.append(textNode)\n      document.querySelector('#app').prepend(div);\n    })\n  ;\n}\n\nauth();\nsetInterval(api, 1111);\n```\n\nwith that, we can verify on http://127.0.0.1:8080 page\nhow frontend applications is automatically doing\nauthentication and accessing rest api!\n\n## step: 5\n\njust adding `JwtRequestFilter` was not enough. last\nmissing peace is spring by default managing state, so\nin certain cases JWT expiration may not work completely.\nto fix that problem we should configure our spring\nsecurity config accordingly:\n\n_MyWebSecurity_\n\n```java\n@Configuration\n@RequiredArgsConstructor\nclass MyWebSecurity extends WebSecurityConfigurerAdapter {\n\n  final JwtRequestFilter jwtRequestFilter;\n\n  @Override\n  protected void configure(HttpSecurity http) throws Exception {\n    // @formatter:off\n    http.authorizeRequests()\n          .mvcMatchers(HttpMethod.GET, \"/\").permitAll()\n          .mvcMatchers(HttpMethod.POST, \"/api/auth\").permitAll()\n          .anyRequest().authenticated()//.fullyAuthenticated()//\n        .and()\n          .csrf().disable()\n        .sessionManagement()\n          .sessionCreationPolicy(SessionCreationPolicy.STATELESS)\n        .and()\n          .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)\n    // @formatter:on\n    ;\n  }\n\n  // ...\n}\n```\n\nnow run application and open http://127.0.0.1:8080/\npage to verify how token will expire and requested\nnew one. done!\n\n## maven\n\nwe will be releasing after each important step! so it will be easy simply checkout needed version from git tag.\nrelease current version using maven-release-plugin (when you are using *-SNAPSHOT version for development):\n\n```bash\ncurrentVersion=`./mvnw -q --non-recursive exec:exec -Dexec.executable=echo -Dexec.args='${project.version}'`\n\n./mvnw build-helper:parse-version -DgenerateBackupPoms=false versions:set \\\n    -DnewVersion=\\${parsedVersion.majorVersion}.\\${parsedVersion.minorVersion}.\\${parsedVersion.nextIncrementalVersion}\ndevelopmentVersion=`./mvnw -q --non-recursive exec:exec -Dexec.executable=echo -Dexec.args='${project.version}'`\n./mvnw build-helper:parse-version -DgenerateBackupPoms=false versions:set -DnewVersion=\"$currentVersion\"\n\n./mvnw clean release:prepare release:perform \\\n    -B -DgenerateReleasePoms=false -DgenerateBackupPoms=false \\\n    -DreleaseVersion=\"$currentVersion\" -DdevelopmentVersion=\"$developmentVersion\"\n```\n\n## resources\n\n* https://github.com/daggerok/spring-security-examples\n* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)\n* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.3.0.M4/maven-plugin/reference/html/)\n* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.3.0.M4/maven-plugin/reference/html/#build-image)\n* [Spring Security](https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/htmlsingle/#boot-features-security)\n* [Spring Configuration Processor](https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/htmlsingle/#configuration-metadata-annotation-processor)\n* [Spring Web](https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications)\n* [Securing a Web Application](https://spring.io/guides/gs/securing-web/)\n* [Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/)\n* [Authenticating a User with LDAP](https://spring.io/guides/gs/authenticating-ldap/)\n* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)\n* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)\n* [Building REST services with Spring](https://spring.io/guides/tutorials/bookmarks/)\n* https://www.youtube.com/watch?v=X80nJ5T7YpE\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdaggerok%2Fspring-jwt-secured-apps","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdaggerok%2Fspring-jwt-secured-apps","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdaggerok%2Fspring-jwt-secured-apps/lists"}