{"id":4005,"date":"2026-07-30T03:59:11","date_gmt":"2026-07-30T03:59:11","guid":{"rendered":"https:\/\/tucumandevelopers.com\/index.php\/2026\/07\/30\/secure-your-apis-oauth2-and-jwt-for-beginners\/"},"modified":"2026-07-30T03:59:11","modified_gmt":"2026-07-30T03:59:11","slug":"secure-your-apis-oauth2-and-jwt-for-beginners","status":"publish","type":"post","link":"https:\/\/tucumandevelopers.com\/index.php\/2026\/07\/30\/secure-your-apis-oauth2-and-jwt-for-beginners\/","title":{"rendered":"Secure Your APIs: OAuth2 and JWT for Beginners"},"content":{"rendered":"<div>\n<div>\n<section data-clarity-region=\"article\">\n<div>\n<p><a href=\"\/kotlin\/category\/backend\/\">Backend<\/a> <a href=\"\/kotlin\/category\/news\/\">News<\/a> <a href=\"\/kotlin\/category\/tutorials\/\">Tutorials<\/a><\/p>\n<p><em>This tutorial was written by an external contributor.<\/em><\/p>\n<div>\n<div>\n<h4>Mdu Sibisi<\/h4>\n<p data-start=\"74\" data-end=\"526\">Mdu Sibisi is an Oracle-certified software developer and blogger with over ten years of experience working primarily with object-oriented languages. He has been writing about technology for more than eight years, focusing on making complex topics easier to understand. Mdu is passionate about accessible developer education, clean code, and creating content that helps developers learn and grow.<\/p>\n<p><a href=\"https:\/\/www.technewstoday.com\/author\/mduduzi\/\" target=\"_blank\" rel=\"noopener\">Website<\/a> | <a href=\"https:\/\/x.com\/Old_Recluse\" target=\"_blank\" rel=\"noopener\">Twitter<\/a><\/p>\n<\/p><\/div>\n<\/p><\/div>\n<div>\n<article>\n<div>\n<h3>Repository with the companion code for the tutorial<\/h3>\n<p><a href=\"https:\/\/kotl.in\/6uptzh\" target=\"_blank\" rel=\"noopener\">Go to GitHub<\/a> <\/p>\n<\/div>\n<\/article><\/div>\n<p>APIs are frequent targets for bad actors since they expose data and functionality. Securing them while maintaining usability is often one of the most challenging and time-consuming parts of API development.<a href=\"https:\/\/oauth.net\/2\/\" target=\"_blank\" rel=\"noreferrer noopener\"> OAuth 2.0<\/a> and<a href=\"https:\/\/jwt.io\/\" target=\"_blank\" rel=\"noreferrer noopener\"> JSON Web Tokens<\/a> (JWT) help make these processes more manageable and reliable. They allow developers to represent and verify identity and manage access by safely transmitting claims and enabling delegated authorization.<\/p>\n<p>This article discusses these technologies and the most efficient ways you can use them to secure your Spring Boot-built APIs and backends. If you\u2019re interested in a coroutine\u2011driven solution, a companion tutorial using <a href=\"https:\/\/ktor.io\/\" target=\"_blank\" rel=\"noreferrer noopener\">Ktor<\/a> is also planned and will be published soon.<\/p>\n<h2>OAuth2 and JWT Primer<\/h2>\n<p>OAuth2 and JWT(s) aren\u2019t competing technologies. They\u2019re complementary pieces of the puzzle, with one handling the delegation of authorization and the other serving as the compact, verifiable token format that carries secure information.<\/p>\n<h3>Authentication vs. Authorization<\/h3>\n<p>Authentication verifies identity (who you are), usually through credentials like passwords, tokens, or certificates. JWTs can carry identity information and act like a form of ID once issued. Roles and other claims within a JWT are then used for authorization.<\/p>\n<p>Authorization helps control what a user has access to (what they can do). This includes the scopes or resources that they can \u201ctouch\u201d and how those permissions are managed. In a system that uses OAuth2 and JWT, the access badge is bundled into your ID card. OAuth2 oversees and manages this process.<\/p>\n<h3>The Role of OAuth2<\/h3>\n<p>OAuth2 is a framework for delegated access. Instead of sharing passwords directly, users grant applications a token that represents their permissions. This means that your backend (acting as a<a href=\"https:\/\/www.oauth.com\/oauth2-servers\/the-resource-server\/\" target=\"_blank\" rel=\"noreferrer noopener\"> Resource Server<\/a>) doesn\u2019t have to issue tokens. Instead, it trusts and validates the ones coming from the Authorization Server within OAuth2\u2019s framework. This decoupling of duties allows you to simplify your APIs while reducing security risks and ensuring all tokens follow a clear, consistent, centralized policy.<\/p>\n<p>You don\u2019t have to worry about implementing user logins or browser redirects within your API. As far as validation and authorization are concerned, your backend or API\u2019s job is to receive the<a href=\"https:\/\/blog.postman.com\/what-is-a-bearer-token\/\" target=\"_blank\" rel=\"noreferrer noopener\"> Bearer Token<\/a>, authenticate the signature, check expiration, and enforce scopes\/roles.<\/p>\n<p>Your API just checks badges; it\u2019s not responsible for printing them. So how do JWTs fit into the equation?<\/p>\n<h3>What Is a JWT?<\/h3>\n<p>A JWT is a small, web-friendly piece of text (string) that securely transports information between systems. Their compactness makes them easy to pass around in<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Reference\/Headers\" target=\"_blank\" rel=\"noreferrer noopener\"> HTTP headers<\/a> or URLs. Each token uses<a href=\"https:\/\/nshielddocs.entrust.com\/wsop-docs\/user-guide\/base64url-encoding.html\" target=\"_blank\" rel=\"noreferrer noopener\"> Base64URL encoding<\/a>, making them safe to include in query strings or headers.<\/p>\n<p>JWTs are signed (and sometimes encrypted), so that recipients can verify that they weren\u2019t tampered with. They\u2019re also self-contained, carrying details like user ID, roles, or permissions. These elements (especially self-containment and signing) allow for<a href=\"https:\/\/www.descope.com\/learn\/post\/stateless-authentication\" target=\"_blank\" rel=\"noreferrer noopener\"> stateless authentication<\/a> without<a href=\"https:\/\/dev.to\/aneeqakhan\/a-developers-guide-to-browser-storage-local-storage-session-storage-and-cookies-4c5f#:~:text=2.%20Session%20Storage%20%E2%8F%B3\" target=\"_blank\" rel=\"noreferrer noopener\"> Session Storage<\/a>. This means that you don\u2019t need a database or cache to track active sessions. It also encourages fewer lookups and less infrastructure complexity, which reduces your system\u2019s overhead.<\/p>\n<p>JWTs have a very simple, standardized structure made up of three parts, separated by dots:<\/p>\n<ul>\n<li><strong>The Header<\/strong> contains metadata about the token, such as the type (<code>JWT<\/code>) and the signing algorithm (<code>HS256<\/code>, <code>RS256<\/code>).<\/li>\n<li><strong>The Payload<\/strong> features the claims, which are statements about the user or system (like user ID, roles, or token expiry).<\/li>\n<li><strong>The Signature<\/strong> is a cryptographic signature created using the header, payload, and a secret or private key. This ensures the token has not been tampered with.<\/li>\n<\/ul>\n<p>The basic structure of a JWT looks like this:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">xxxxx.yyyyy.zzzzz<\/pre>\n<p>A real-world Base64URL-encoded token typically resembles the following:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c<\/pre>\n<h3>When OAuth2 meets JWT<\/h3>\n<p>There are four key roles in OAuth2\u2019s implementation:<\/p>\n<ul>\n<li><strong>Resource Owner:<\/strong> The entity (usually the user) granting access to the protected resources.<\/li>\n<li><strong>Client:<\/strong> The application requesting access to the resource on behalf of the resource owner.<\/li>\n<li><strong>Authorization Server:<\/strong> The server that authenticates the resource owner and issues access tokens to the client.<\/li>\n<li><strong>Resource Server:<\/strong> The server hosting the protected resources, which accepts and validates tokens.<\/li>\n<\/ul>\n<p>The Resource Owner grants permission (<em>e.g.<\/em>, you click \u201cAllow\u201d when an app requests access), the Client then requests authorization from the Authorization Server, which issues an access token (JWT) if the Resource Owner approves. The Client uses this access token to access data from the Resource Server.<\/p>\n<p><strong>Note:<\/strong> It\u2019s important to note that JWTs aren\u2019t the only token format that OAuth2 can work with; it\u2019s just the most popular because of its perks. OAuth2 can also work with <a href=\"https:\/\/docs.secureauth.com\/ciam\/en\/opaque-token--concept,-purpose,-way-it-works.html\" target=\"_blank\" rel=\"noopener\">Opaque Tokens<\/a>, <a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/framework\/wcf\/feature-details\/saml-tokens-and-claims\" target=\"_blank\" rel=\"noopener\">SAML Tokens<\/a>, or custom token formats like Microsoft\u2019s reference tokens or Google\u2019s access tokens.<\/p>\n<h2>How to Implement OAuth2 and JWT<\/h2>\n<p>Imagine you\u2019re building a simple document management system with a Kotlin and Spring-based backend that exposes a REST API. This implementation lets clients upload documents, list them, view specific ones, etc. Some potential endpoints the API can expose include:<\/p>\n<ul>\n<li><code>GET \/documents<\/code>: Lists all documents.<\/li>\n<li><code>GET \/documents\/{id}<\/code>: View a specific document.<\/li>\n<li><code>POST \/documents<\/code>: Upload a new document.<\/li>\n<\/ul>\n<p>You want to restrict access so that only authenticated users can view or upload documents, but you don\u2019t want to manage passwords in your backend. You also don\u2019t have to maintain sessions or deal with login forms.<\/p>\n<h3>Prerequisites<\/h3>\n<p>If you want to follow along, you\u2019ll need:<\/p>\n<ul>\n<li><a href=\"https:\/\/www.jetbrains.com\/idea\/download\/\" target=\"_blank\" rel=\"noreferrer noopener\">IntelliJ IDEA<\/a><\/li>\n<li><a href=\"https:\/\/jdk.java.net\/17\/\" target=\"_blank\" rel=\"noreferrer noopener\">JDK 17+<\/a><\/li>\n<li><a href=\"https:\/\/console.cloud.google.com\/welcome\/new\" target=\"_blank\" rel=\"noreferrer noopener\">Google Cloud Console<\/a><\/li>\n<li>A basic understanding of <a href=\"https:\/\/kotlinlang.org\/docs\/getting-started.html\" target=\"_blank\" rel=\"noreferrer noopener\">Kotlin<\/a>, Spring Boot, and Spring Security<\/li>\n<\/ul>\n<p>All the code used in this tutorial is available on <a href=\"https:\/\/github.com\/OrigamiFolds\/doc-manager-kotlin-demo\" target=\"_blank\" rel=\"noreferrer noopener\">GitHub repository<\/a>.<\/p>\n<h3>Initial Application Setup<\/h3>\n<p>To start, run IntelliJ IDEA and create a new project (<strong>File<\/strong> &gt; <strong>New<\/strong> &gt; <strong>Project<\/strong>):<\/p>\n<figure><\/figure>\n<p>Select <strong>Spring Boot<\/strong> under the Generators section on the left panel. Give your project a name (like <code>doc-manager<\/code>), select <strong>Kotlin<\/strong> as the Language, <strong>Gradle \u2013 Kotlin<\/strong> as the Type, <strong>17<\/strong> as the Java version, <strong>Jar<\/strong> as the packaging, and <strong>Properties<\/strong> as the configuration. Leave all other properties in their default state and then click <strong>Next<\/strong>.<\/p>\n<figure><\/figure>\n<p>On the next screen, select dependencies for your project. Make sure you\u2019re using the latest stable version of Spring Boot (4.0.3 at the time of writing) and then use the search bar to find and add the following dependencies:<\/p>\n<ul>\n<li>Spring Security<\/li>\n<li>OAuth2 Authorization Server<\/li>\n<li>OAuth2 Resource Server<\/li>\n<li>Spring Web<\/li>\n<\/ul>\n<p>Once that\u2019s done, click <strong>Create<\/strong>.<\/p>\n<figure><\/figure>\n<p>After your project\u2019s done importing and loading, expand your project, scroll down, and find the <code>application.properties<\/code> file under the resources folder (<strong>src<\/strong> &gt; <strong>main<\/strong> &gt; <strong>resources<\/strong>). Add the following lines to it:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">spring.application.name=doc-manager-kotlin-demo spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public.pem<\/pre>\n<p>In most cases, you\u2019d specify an<a href=\"https:\/\/docs.spring.io\/spring-security\/reference\/servlet\/oauth2\/resource-server\/jwt.html#_specifying_the_authorization_server\" target=\"_blank\" rel=\"noreferrer noopener\"> Authorization Server<\/a> (<code>issuer-uri<\/code>) here. But to keep things simple, you won\u2019t be using a real Authorization Server for this part of the implementation (this will come in later). So you need to supply your application with a public key to verify signed tokens. You can generate your own <code>publickey.pem<\/code> using<a href=\"https:\/\/www.scottbrady.io\/openssl\/creating-rsa-keys-using-openssl\" target=\"_blank\" rel=\"noreferrer noopener\"> OpenSSL<\/a> or use the ones provided in this project\u2019s<a href=\"https:\/\/github.com\/OrigamiFolds\/doc-manager-kotlin-demo\/tree\/master\/src\/main\/resources\" target=\"_blank\" rel=\"noreferrer noopener\"> resources folder<\/a>. Make sure to save and store the <code>private.pem<\/code>. You\u2019ll need it for JWT generation.&nbsp;<\/p>\n<h3>Configure Your Resource Server<\/h3>\n<p>Create a resource controller for your endpoint:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ Insert Your Package Name Here + .controller import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping(\"\/api\") class ResourceController { @GetMapping(\"\/fetchDocuments\") fun fetchDocumentsEndpoint(): String { return \"Here are your documents\" } }<\/pre>\n<p>For now, the <code>ResourceController<\/code> class contains only one endpoint.<\/p>\n<p>Next, create a security configuration for your Resource Server:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ Insert Your Package Name Here + .config import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.web.SecurityFilterChain @Configuration @EnableWebSecurity class OAuth2ResourceServerSecurityConfiguration { @Bean @Throws(Exception::class) fun securityFilterChain(http: HttpSecurity): SecurityFilterChain = http .httpBasic { it.disable() } .formLogin { it.disable() } \/\/ Disables Spring's default form-based login .csrf { it.disable() } .authorizeHttpRequests { it.requestMatchers(HttpMethod.GET, \"\/api\/fetchDocuments\").hasAuthority(\"SCOPE_read:documents\") \/\/ Verifies that client has read access it.anyRequest().authenticated() } .oauth2ResourceServer { \/\/ Enables JWT\u2011based authentication for an OAuth2 Resource Server. it.jwt { } } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } .build() }<\/pre>\n<p>If you\u2019ve worked with Spring Security in Java before, you\u2019ll likely notice how clean the Kotlin DSL looks in comparison. References to <code>OAuth2LoginConfigurer<\/code>, wrapping lambdas in <code>Customizer<\/code>, or even annotations like <code>@Throws(Exception::class)<\/code> aren\u2019t strictly necessary (unless you\u2019re working with a mix of Java and Kotlin). Kotlin\u2019s DSL trims that away and lets you express the rules directly.<\/p>\n<p>Now, generate the JWT using the private key (found in the <code>private.pem<\/code>). Make sure to encode it using the <code>RS256<\/code> and that the claims are set and formatted correctly:<\/p>\n<figure><\/figure>\n<p>Run your Spring Boot application and then initiate an authenticated request to the <code>\/fetchDocuments<\/code> API endpoint with your generated JWT as the bearer token:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">GET http:\/\/localhost:8080\/api\/fetchDocuments Bearer Token &lt;JWT&gt;<\/pre>\n<figure><\/figure>\n<p>If it works as it should, you should see \u201cHere are your documents\u201d as a response. This implementation enables you to simulate a client sending a request with a Bearer token (JWT). Upon receiving the token, your Resource Server (backend) checks the expiry date and signature using the details in your application\u2019s properties file. It also looks for the <code>read:documents<\/code> scope before granting access to the <code>fetchDocument<\/code> endpoint.<\/p>\n<h2>Handling Advanced Patterns and Validations<\/h2>\n<p>In a document management API (and most complex systems), simple scope checks aren\u2019t enough. They can grant coarse permissions, but they often fail to capture the nuance of real-world access control. To address this, the system must separate token validation (ensuring the JWT is authentic) from business authorization (deciding what actions a user can perform).<\/p>\n<p>Scopes alone can\u2019t enforce ownership or hierarchical rules, and they don\u2019t capture organizational roles. That\u2019s why you need a combination of scope and role-based access, where administrators can access all features, while lower-level users are granted only a few. By layering roles, scopes, and resource checks, the API achieves fine-grained, context-aware authorization that balances security with usability.<\/p>\n<p>Hardcoding security decisions in such systems should be avoided at all costs. Practices like embedding role checks or scope logic directly into controller methods may seem convenient at first, but it introduces significant risks as your system grows. A developer might forget to update one of these hardcoded checks when business requirements change, leaving certain endpoints exposed or inconsistent. Hardcoding also undermines separations of concerns. Security decisions should be modeled in a dedicated layer, not mixed into business logic.<\/p>\n<h3>Using Custom Claim Extraction and Spring Security\u2019s PreAuthorize<\/h3>\n<p>Like most token formats, JWTs can carry custom claims in their payloads. A JWT with custom claims for roles and permissions would look something like this: &nbsp;<\/p>\n<pre data-enlighter-language=\"json\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">{ \"iss\": \"https:\/\/myapp.com\/auth\", \"sub\": \"mdu\", \"iat\": 1773754406, \"exp\": 1773840838, \"scope\": \"read:documents\", \"roles\": [\"admin\", \"editor\"], \"permissions\": [\"documents:read:all\", \"documents:write:own\"] }<\/pre>\n<p>Spring handles authority mapping for scopes out of the box and provides a <code>hasRole<\/code> function. However, roles aren\u2019t automatically extracted from JWTs because there is no universal standard for how identity providers represent them. Scopes are standardized in OAuth2 and OpenID Connect, so Spring can safely map them into authorities. Roles often appear under custom claims and require a custom converter to translate them into Spring\u2019s expected format before they can be used effectively.<\/p>\n<p>Let\u2019s say you want to authenticate and authorize based on roles and scope. Navigate to your security config and add the following function:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@Bean fun jwtAuthenticationConverter(): JwtAuthenticationConverter { val converter = JwtAuthenticationConverter() converter.setJwtGrantedAuthoritiesConverter { jwt -&gt; val authorities = mutableListOf&lt;GrantedAuthority&gt;() \/\/ Map scopes val scopes = (jwt.claims[\"scope\"] as? String)?.split(\" \") ?: emptyList() authorities.addAll(scopes.map { SimpleGrantedAuthority(\"SCOPE_$it\") }) \/\/ Map roles val roles = jwt.claims[\"roles\"] as? Collection&lt;*&gt; ?: emptyList&lt;Any&gt;() authorities.addAll(roles.map { SimpleGrantedAuthority(\"ROLE_$it\") }) \/\/ Map permissions val permissions = jwt.claims[\"permissions\"] as? Collection&lt;*&gt; ?: emptyList&lt;Any&gt;() authorities.addAll(permissions.map { SimpleGrantedAuthority(it.toString()) }) authorities } return converter }<\/pre>\n<p>This changes the behaviour of the <code>JwtAuthenticationConverter<\/code> so that it no longer relies solely on Spring Security\u2019s default scope mapping. Instead, it explicitly maps both scopes and roles from the JWT into Spring authorities. If you mapped only roles, then Spring Security would ignore the <code>scope<\/code> claim entirely.<\/p>\n<p>Kotlin ensures the safe extraction of custom claims thanks to its null-safety. For instance, take a look at the scope mapping section of the code. The safe call operator (<code>?.<\/code>) ensures that if <code>jwt.claims[\"scope\"]<\/code> is <code>null<\/code>, the chain stops gracefully instead of throwing a <code>NullPointerException<\/code>. The safe cast operator (<code>as? String<\/code>) attempts to convert the value returned from the <code>jwt.claims[\"scope\"]<\/code> operation into a <code>String<\/code> from an <code>Any?<\/code> (could be anything or null). If the safe cast operator fails, it returns <code>null<\/code> instead of throwing a <code>ClassCastException<\/code>. This allows for type-safe conversions that won\u2019t interrupt or break your code. The Elvis operator (<code>?:<\/code>) provides a fallback value when the left-hand side is <code>null<\/code>. So if the role is missing for whatever reason, the function returns an empty list as a default value.<\/p>\n<p>The tricky part is adding validations for all these claims. If you were checking these claims individually, you could use the <code>hasRole<\/code> function for roles, and <code>hasAuthority<\/code> for scopes and permissions. One way to chain these validations together would be to use the<a href=\"https:\/\/docs.spring.io\/spring-security\/reference\/api\/java\/org\/springframework\/security\/config\/annotation\/web\/configurers\/AuthorizeHttpRequestsConfigurer.AuthorizedUrl.html#access(org.springframework.security.authorization.AuthorizationManager)\" target=\"_blank\" rel=\"noreferrer noopener\"> access<\/a> function. Here, you\u2019ll use [Spring\u2019s Method Security](<a href=\"https:\/\/www.baeldung.com\/spring-enablemethodsecurity\" target=\"_blank\" rel=\"noreferrer noopener\">Spring @EnableMethodSecurity Annotation | Baeldung<\/a>) (<code>@PreAuthorize<\/code>) because it offers a more fine-grained and cleaner approach.<\/p>\n<p>Return to your Security Config file and place the <code>@EnableMethodSecurity(prePostEnabled = true)<\/code> above the class definition:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">... import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) class OAuth2ResourceServerSecurityConfiguration { class SecurityConfig( ... <\/pre>\n<p>You can keep your security filter chain as is for now. Navigate to your resource controller, and add the <code>@PreAuthorize<\/code> annotation to it:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">... @GetMapping(\"\/fetchDocuments\") @PreAuthorize(\"hasRole('admin') and hasAuthority('documents:read:all')\") fun fetchDocumentsEndpoint(): String { return \"Here are your documents\" } ...<\/pre>\n<p><strong>Note:<\/strong> You\u2019ll need to import the <a href=\"https:\/\/docs.spring.io\/spring-security\/site\/apidocs\/org\/springframework\/security\/access\/prepost\/PreAuthorize.html\" target=\"_blank\" rel=\"noopener\">PreAuthorize<\/a> annotation for this to work. <\/p>\n<p>This ensures that only admins with read-all permissions can access the <code>fetchDocuments<\/code> endpoint. You can create more endpoints, like <code>getDocument<\/code> and <code>deleteDocument<\/code> to test the combination of your roles and permissions. The <code>@PreAuthorize<\/code> annotation helps you avoid embedding role checks or scope logic directly inside controller methods (for example, writing <code>if (user.hasRole(\"admin\")) { ... }<\/code> in the body of a controller). Alternatively, you can perform your role checks in your filter chain and your scope and permission checks on the method level.<\/p>\n<h3>Strengthening Token Trust: Issuer and Audience Enforcement<\/h3>\n<p>Under most normal circumstances, you\u2019d supply Spring Security with an issuer URI in your application properties file. Then Spring would do the work of finding the <a href=\"https:\/\/openid.net\/specs\/openid-connect-discovery-1_0.html#ProviderConfig\" target=\"_blank\" rel=\"noreferrer noopener\">Provider Configuration<\/a> or<a href=\"https:\/\/tools.ietf.org\/html\/rfc8414#section-3\" target=\"_blank\" rel=\"noreferrer noopener\"> Authorization Server Metadata<\/a> and using them to decode your JWT. But as you learned here, these can be bypassed when you\u2019re using custom-generated keys.<\/p>\n<p>Regardless of whether you\u2019ve configured an issuer URI or not, it\u2019s important to explicitly verify the issuer (<code>iss<\/code>) in your code to ensure that every incoming token actually claims the same issuer and prevent token replay across apps. This adds defense in depth and makes your security posture clear in code. Likewise, audience (<code>aud<\/code>) ensures that the token is meant for your API, not for some other application. When both the issuer and the audience are checked, it prevents tokens from other apps or environments from being accepted by your API.<\/p>\n<p>To validate these claims, you\u2019ll need to create a custom <a href=\"https:\/\/docs.spring.io\/spring-security\/reference\/api\/java\/org\/springframework\/security\/oauth2\/jwt\/JwtDecoder.html\" target=\"_blank\" rel=\"noreferrer noopener\">JwtDecoder<\/a>. But, because Spring doesn\u2019t have a dedicated <a href=\"https:\/\/docs.spring.io\/spring-security\/reference\/api\/java\/org\/springframework\/security\/oauth2\/core\/OAuth2TokenValidator.html\" target=\"_blank\" rel=\"noreferrer noopener\">OAuth2TokenValidator<\/a> for its audience, you\u2019ll need to create one. Re-open your Security Configuration file and add the following class (nested):<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">class AudienceValidator(private val audience: String) : OAuth2TokenValidator&lt;Jwt&gt; { override fun validate(token: Jwt): OAuth2TokenValidatorResult = if (token.audience.contains(audience)) { OAuth2TokenValidatorResult.success() } else { OAuth2TokenValidatorResult.failure(OAuth2Error(\"invalid_token\", \"The required audience is missing\", null)) } }<\/pre>\n<p><strong>Warning:<\/strong> Don\u2019t forget to import all necessary classes and interfaces<\/p>\n<p>Then, add the following method:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@Bean fun jwtDecoder(): JwtDecoder { val issuer = \"https:\/\/myapp.com\/auth\" \/\/ Replace with your own official issuer URI val audience = \"http:\/\/localhost:8080\/api\/\" val decoder = JwtDecoders.fromIssuerLocation&lt;NimbusJwtDecoder&gt;(issuer) \/\/ Add audience validation val audienceValidator = AudienceValidator(audience) val issuerValidator = JwtValidators.createDefaultWithIssuer(issuer) val validator = DelegatingOAuth2TokenValidator(listOf(issuerValidator, audienceValidator)) (decoder as NimbusJwtDecoder).setJwtValidator(validator) return decoder }<\/pre>\n<p>This function builds a custom <code>JwtDecoder<\/code> that enforces stricter validation on incoming JWTs. It starts by creating a decoder from the configured issuer, then defines two validators: one to ensure the token\u2019s <code>aud<\/code> claim matches the expected audience, and another to ensure the <code>iss<\/code> claim matches the trusted issuer. These validators are combined into a <code>DelegatingOAuth2TokenValidator<\/code> and applied to the decoder, so that only tokens issued by the correct identity provider and intended for your application are accepted.<\/p>\n<p><strong>Note:<\/strong> If you need an Authorization Server (issuer) to test this flow, you can use a local or mock server like <a href=\"https:\/\/github.com\/navikt\/mock-oauth2-server?tab=readme-ov-file\" target=\"_blank\" rel=\"noopener\">mock-oauth2-server<\/a>. It also supports custom JWT generation. <\/p>\n<p>Add the validation to your security filter chain:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@Bean @Throws(Exception::class) fun securityFilterChain(http: HttpSecurity): SecurityFilterChain = http .httpBasic { it.disable() } .formLogin { it.disable() } .csrf { it.disable() } .authorizeHttpRequests { it.requestMatchers(\"\/api\/fetchDocuments\").hasAuthority(\"SCOPE_read:documents\") it.anyRequest().authenticated() } .oauth2ResourceServer { it.jwt { jwt -&gt; jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()) jwt.decoder(jwtDecoder()) \/\/ Add custom JwtDecoder } } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } .build()<\/pre>\n<p>This allows the strict enforcement of the rules by the backend, never leaving it up to frontend logic to authorize or validate sensitive information.<\/p>\n<div>\n<article>\n<div>\n<h3>Repository with the companion code for the tutorial<\/h3>\n<p><a href=\"https:\/\/kotl.in\/6uptzh\" target=\"_blank\" rel=\"noopener\">Go to GitHub<\/a> <\/p>\n<\/div>\n<\/article><\/div>\n<h2>What\u2019s Next?<\/h2>\n<p>Strong security requires fine-grained control and layered safeguards beyond basic authentication. Use short-lived tokens with clear refresh and revocation strategies to limit exposure and prevent compromised tokens from persisting. Avoid using JWTs for session storage, as this leads to token bloat, complicates revocation, and increases the risk of exposing sensitive data. Instead, keep JWTs focused on authentication and authorization claims, and enforce validation of issuer, audience, signature, and expiry to ensure tokens are trustworthy and intended for your application.<\/p>\n<p>Ultimately, securing Spring Boot APIs with OAuth2 and JWT depends on careful design, explicit configuration, and a clear understanding of how tokens, scopes, and identities are validated and enforced. Kotlin complements this by promoting null safety, immutability, and concise configuration, helping reduce misconfigurations and overlooked edge cases.<\/p>\n<\/p><\/div>\n<p> <a href=\"#\"><\/a> <\/section>\n<div>\n<p><h2>Discover more<\/h2>\n<\/p><\/div>\n<\/p><\/div>\n<\/div>\n<\/div>\n<\/div>\n<p>Fuente: <a href=\"https:\/\/blog.jetbrains.com\/kotlin\/2026\/07\/secure-your-apis-oauth2-and-jwt-for-beginners\/\">Art\u00edculo original<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Backend News Tutorials This tutorial was written by an external contributor. Mdu Sibisi Mdu Sibisi is an Oracle-certified software developer and blogger with over ten years of experience working primarily with object-oriented languages. He has been writing about technology for more than eight years, focusing on making complex topics easier to understand. Mdu is passionate [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":4004,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[46],"tags":[],"class_list":["post-4005","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-jetbrain"],"jetpack_publicize_connections":[],"_links":{"self":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/4005","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/comments?post=4005"}],"version-history":[{"count":0,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/4005\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media\/4004"}],"wp:attachment":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media?parent=4005"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/categories?post=4005"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/tags?post=4005"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}