{"id":15,"date":"2024-09-12T19:47:59","date_gmt":"2024-09-12T19:47:59","guid":{"rendered":"https:\/\/marcinzygmunt.pl\/blog\/?p=15"},"modified":"2025-09-24T18:44:29","modified_gmt":"2025-09-24T18:44:29","slug":"extending-jwt-authentication-with-refresh-tokens-in-spring-boot","status":"publish","type":"post","link":"https:\/\/marcinzygmunt.pl\/blog\/extending-jwt-authentication-with-refresh-tokens-in-spring-boot\/","title":{"rendered":"Extending Simple JWT Authentication with Refresh Tokens with Spring Boot (DB version)"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\"><\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">If you\u2019re already familiar with the [basic JWT setup], this project shows how to <strong>safely refresh expired tokens<\/strong> while keeping your API secure and managing long-lived user sessions. <br>When token expires you don&#8217;t need to login again and send full credentials &#8211; just use refresh token instead to get another set of access and refresh tokens.<\/p>\n\n\n\n<!--more-->\n\n\n\n<p class=\"wp-block-paragraph\"><strong>TIP:<\/strong> Storing refresh tokens in the database also allows them to be shared across multiple instances of the application.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Like the previous example, this project is built with <strong>Spring Boot 3<\/strong>.<\/p>\n\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;6aac8caf56011&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"6aac8caf56011\" class=\"wp-block-image size-large wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"873\" height=\"1024\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/marcinzygmunt.pl\/blog\/wp-content\/uploads\/2025\/09\/jwt_2-873x1024.png\" alt=\"\" class=\"wp-image-25\" srcset=\"https:\/\/marcinzygmunt.pl\/blog\/wp-content\/uploads\/2025\/09\/jwt_2-873x1024.png 873w, https:\/\/marcinzygmunt.pl\/blog\/wp-content\/uploads\/2025\/09\/jwt_2-256x300.png 256w, https:\/\/marcinzygmunt.pl\/blog\/wp-content\/uploads\/2025\/09\/jwt_2-768x901.png 768w, https:\/\/marcinzygmunt.pl\/blog\/wp-content\/uploads\/2025\/09\/jwt_2.png 1044w\" sizes=\"auto, (max-width: 873px) 100vw, 873px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\tdata-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.thisImage.buttonRight\"\n\t\t\tdata-wp-style--top=\"state.thisImage.buttonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">What\u2019s included in the project<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Basic Auth + JWT<\/strong>: Users log in with credentials and receive a JWT token.<\/li>\n\n\n\n<li><strong>User data and Refresh Tokens stored in the database<\/strong>: Credentials and refresh tokens are stored in PostgreSQL, allowing secure session renewal.<\/li>\n\n\n\n<li><strong>Swagger UI<\/strong>: Explore and test endpoints at <code>http:\/\/localhost:8088\/swagger-ui\/index.html<\/code>.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Setup in 4 Steps<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Clone the repository<\/strong><\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>git clone https:\/\/github.com\/marcinzygmunt-pl\/spring-boot-jwt-with-refresh.git\ncd spring-boot-jwt-with-refresh<\/code><\/pre>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li><strong>Configure PostgreSQL<\/strong><br>Update your database credentials in the <code>application.yml<\/code> file.<\/li>\n\n\n\n<li><strong>Run the application<\/strong><\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>.\/mvnw spring-boot:run<\/code><\/pre>\n\n\n\n<ol start=\"4\" class=\"wp-block-list\">\n<li><strong>Test endpoints in Swagger UI<\/strong><br>Open <code>http:\/\/localhost:8088\/swagger-ui\/index.html<\/code> and check login, token refresh, and access to protected resources.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">See also:<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Refresh in CACHE<\/strong><\/li>\n\n\n\n<li><strong>Refresh in HAZELCAST (multi-instance setup)<\/strong><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>If you\u2019re already familiar with the [basic JWT setup], this project shows how to safely refresh expired tokens while keeping your API secure and managing long-lived user sessions.\u2026<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1],"tags":[9,5],"class_list":["post-15","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-jwt","tag-springboot"],"jetpack_likes_enabled":true,"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/marcinzygmunt.pl\/blog\/wp-json\/wp\/v2\/posts\/15","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/marcinzygmunt.pl\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/marcinzygmunt.pl\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/marcinzygmunt.pl\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/marcinzygmunt.pl\/blog\/wp-json\/wp\/v2\/comments?post=15"}],"version-history":[{"count":7,"href":"https:\/\/marcinzygmunt.pl\/blog\/wp-json\/wp\/v2\/posts\/15\/revisions"}],"predecessor-version":[{"id":154,"href":"https:\/\/marcinzygmunt.pl\/blog\/wp-json\/wp\/v2\/posts\/15\/revisions\/154"}],"wp:attachment":[{"href":"https:\/\/marcinzygmunt.pl\/blog\/wp-json\/wp\/v2\/media?parent=15"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/marcinzygmunt.pl\/blog\/wp-json\/wp\/v2\/categories?post=15"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/marcinzygmunt.pl\/blog\/wp-json\/wp\/v2\/tags?post=15"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}