Category: Expert Guide

Does a JWT decoder help in debugging authentication issues?

# The Ultimate Authoritative Guide to JWT Decoders in Debugging Authentication Issues As a Cloud Solutions Architect, I understand the critical importance of robust and efficient authentication mechanisms in modern applications. JSON Web Tokens (JWTs) have become a de facto standard for secure information exchange, particularly in stateless authentication scenarios. However, the complexity and sometimes opaque nature of JWTs can present significant challenges when debugging authentication issues. This guide will delve deep into the role of JWT decoders, specifically focusing on the `jwt-decoder` tool, to help you navigate and resolve authentication problems with confidence. ## Executive Summary In the realm of modern web and cloud-native applications, JWTs are ubiquitous for their ability to securely transmit information between parties as a JSON object. While their stateless nature offers scalability and performance benefits, it also introduces a layer of abstraction that can obscure the root cause of authentication failures. This guide establishes the definitive stance: **Yes, a JWT decoder is an indispensable tool for debugging authentication issues.** Specifically, the `jwt-decoder` tool empowers developers and architects to inspect, validate, and understand the contents of JWTs, thereby accelerating the identification and resolution of common authentication problems. From verifying signature integrity and expiration dates to scrutinizing custom claims, a JWT decoder provides the visibility needed to pinpoint misconfigurations, expired tokens, invalid claims, and other subtle yet critical authentication flaws. This document will explore the technical underpinnings, practical applications, industry standards, and future trajectory of JWT decoding in the context of authentication debugging. ## Deep Technical Analysis: Understanding JWTs and the Role of Decoders ### What is a JWT? A JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. It is composed of three parts separated by dots (`.`): a header, a payload, and a signature. * **Header:** The header typically contains two fields: the type of token (`typ`, which is `JWT`) and the signing algorithm (`alg`), such as `HS256` (HMAC using SHA-256) or `RS256` (RSA Signature with SHA-256). This header is Base64Url encoded. json { "alg": "HS256", "typ": "JWT" } * **Payload:** The payload contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: * **Registered Claims:** These are a set of predefined claims which are not mandatory but recommended. Examples include `iss` (issuer), `exp` (expiration time), `sub` (subject), `aud` (audience), `iat` (issued at), `nbf` (not before), and `jti` (JWT ID). * **Public Claims:** These are custom claims that can be defined by those using JWTs. However, to avoid collision, they should be defined either in a Registry of `https://www.iana.org/assignments/jwt/jwt.xhtml` or be defined as a URI that contains a collision-resistant namespace. * **Private Claims:** These are custom claims created by parties that agree to use them. json { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } The payload is also Base64Url encoded. * **Signature:** The signature is used to verify that the sender of the JWT is who it says it is and to verify that the message hasn't been changed along the way. To create the signature, you take the encoded header, the encoded payload, a secret (for symmetric algorithms like HS256) or a private key (for asymmetric algorithms like RS256), and sign it using the algorithm specified in the header. The signature is also Base64Url encoded. ### The Problem: The "Black Box" of JWTs When a JWT is issued, it's typically sent to the client. The client then includes this token in subsequent requests to the server (usually in the `Authorization` header as `Bearer `). The server's authentication middleware then needs to: 1. **Extract the token** from the request. 2. **Decode the header and payload** (which are just Base64Url encoded strings). 3. **Verify the signature** using the secret or public key and the algorithm specified in the header. 4. **Validate claims** such as expiration time, issuer, and audience. 5. **Grant access** if all checks pass. If authentication fails, the error message might be generic, such as "Unauthorized" or "Invalid Token." Without a way to inspect the token's contents, it becomes incredibly challenging to diagnose *why* it failed. Was the token expired? Was the signature invalid? Is there a missing or incorrect claim? This is where a JWT decoder becomes essential. ### How a JWT Decoder Solves the Problem A JWT decoder acts as a translator, taking a raw JWT string and breaking it down into its constituent parts, making them human-readable and verifiable. The `jwt-decoder` tool, and similar utilities, provide this crucial visibility by: * **Decoding Header and Payload:** They parse the Base64Url encoded header and payload, presenting the JSON objects in a structured and readable format. This allows developers to see the algorithm used, the token type, and all the claims (registered, public, and private) present in the token. * **Signature Verification (Crucial for Debugging):** While a basic decoder might just show the decoded parts, advanced decoders (or the libraries they use) can also perform signature verification. This is paramount for debugging. By providing the correct secret or public key, the decoder can attempt to verify the signature. A failure here immediately points to issues with the secret, the key, the signing algorithm, or tampering with the token. * **Claim Validation Assistance:** By displaying all claims, a decoder helps in manually checking if they match the expected values. For instance, you can visually confirm if the `exp` (expiration time) is in the past, if the `iss` (issuer) matches the expected authority, or if the `aud` (audience) is correct for the intended recipient. * **Identifying Tampering:** If the signature verification fails, it's a strong indicator that the token has been tampered with, or that the wrong key/secret is being used for verification. ### The `jwt-decoder` Tool: A Closer Look The `jwt-decoder` tool (often available as a command-line utility or a library) is designed to simplify this process. While specific implementations might vary, its core functionality revolves around: * **Input:** Accepts a JWT string as input. * **Output:** Displays the decoded header and payload. * **Verification Options:** Often allows specifying the signing algorithm, the secret (for symmetric keys), or the public key (for asymmetric keys) to attempt signature verification. **Example Workflow with `jwt-decoder`:** 1. **Obtain the JWT:** Copy the JWT string from your application's request (e.g., from browser developer tools, Postman, or server logs). 2. **Run `jwt-decoder`:** * **Basic Decoding:** `jwt-decoder ` * **Decoding with Verification:** `jwt-decoder --secret --alg HS256` (for symmetric) or `jwt-decoder --publicKey --alg RS256` (for asymmetric). The output will clearly show the decoded header and payload. If verification is attempted and fails, the tool will typically report a signature verification error, providing a direct clue for debugging. ## Practical Scenarios: Debugging Authentication Issues with `jwt-decoder` This section outlines common authentication issues and how `jwt-decoder` can be instrumental in their resolution. ### Scenario 1: "Unauthorized" Error Due to Expired Token **Problem:** A user suddenly receives an "Unauthorized" error, even though their login session was active moments ago. **Debugging with `jwt-decoder`:** 1. **Obtain the JWT:** Get the JWT from the user's request (e.g., from their browser's local storage or network tab). 2. **Decode the Token:** Run `jwt-decoder `. 3. **Examine the Payload:** Look for the `exp` (expiration time) claim. This claim is a Unix timestamp (seconds since the epoch). 4. **Analyze the Timestamp:** Convert the `exp` timestamp to a human-readable date and time. Many online tools can do this, or you can use programming language functions. 5. **Compare with Current Time:** If the `exp` time is in the past, the token has expired. **Insight:** The `jwt-decoder` immediately reveals the expiration timestamp, allowing you to confirm if this is the cause of the failure. You can then advise the user to re-authenticate or investigate why the token is expiring too quickly (e.g., short-lived tokens, incorrect configuration). ### Scenario 2: Signature Verification Failure ("Invalid Signature" or Generic Error) **Problem:** The server rejects the token with a signature verification error, or a generic "Invalid Token" message. **Debugging with `jwt-decoder`:** 1. **Obtain the JWT:** Get the token from the client. 2. **Identify Signing Details:** Note the `alg` from the decoded header (if you can perform basic decoding first) or from the JWT structure itself. 3. **Attempt Verification:** * **Symmetric Algorithm (e.g., HS256):** Run `jwt-decoder --secret --alg HS256`. Ensure you are using the *exact* secret key the server uses for signing. * **Asymmetric Algorithm (e.g., RS256):** Run `jwt-decoder --publicKey --alg RS256`. Ensure you are using the correct public key that corresponds to the private key used for signing. 4. **Analyze the Output:** * If verification succeeds, the issue might lie elsewhere (e.g., claim validation). * If verification fails, consider the following: * **Incorrect Secret/Key:** The most common cause. Double-check that the secret or public key provided to the decoder exactly matches what the server is configured with. Typos, missing characters, or incorrect encoding are frequent culprits. * **Wrong Algorithm:** Ensure the `--alg` parameter matches the `alg` in the JWT header. * **Token Tampering:** If the token was modified on the client-side after being issued, the signature will no longer match. * **Clock Skew (for `nbf` or `exp` related issues that might manifest as verification issues in some libraries):** While less common for direct signature failure, significant clock differences can sometimes impact validation logic. **Insight:** The `jwt-decoder`'s ability to perform signature verification with provided secrets/keys is invaluable. It isolates signature-related problems, guiding you to check your key management and secret configurations. ### Scenario 3: Missing or Incorrect Claims **Problem:** A user is authenticated but denied access to specific resources, or experiences unexpected behavior. The server logic relies on specific claims within the JWT to authorize actions. **Debugging with `jwt-decoder`:** 1. **Obtain the JWT:** Get the token from the client. 2. **Decode the Token:** Run `jwt-decoder `. 3. **Examine the Payload:** Carefully review all claims in the payload. 4. **Compare with Expected Claims:** * **Role-based Access Control (RBAC):** Check for claims like `roles`, `permissions`, or `scope`. Are they present? Do they contain the expected values (e.g., `["admin", "user"]`)? * **Tenant Identification:** If your application is multi-tenant, check for a `tenant_id` or similar claim. Is it present and correct for the user's context? * **User Information:** Verify claims like `sub` (subject/user ID), `email`, or `username`. Are they as expected? * **Audience (`aud`):** Ensure the `aud` claim correctly identifies the intended recipient of the token. If the token is meant for `api.example.com`, the `aud` should reflect that. **Insight:** `jwt-decoder` provides a clear, formatted view of all claims. This allows for direct comparison against the expected claims defined in your authorization logic, quickly revealing discrepancies or missing information. ### Scenario 4: Issues with `nbf` (Not Before) Claim **Problem:** A newly issued token is being rejected by the server. **Debugging with `jwt-decoder`:** 1. **Obtain the JWT:** Get the token. 2. **Decode the Token:** Run `jwt-decoder `. 3. **Examine the Payload:** Look for the `nbf` claim. This claim specifies the time before which the JWT must not be accepted. 4. **Compare with Current Time:** Convert the `nbf` timestamp to a human-readable date and time. If the current time is *before* the `nbf` time, the token is considered invalid. **Insight:** Similar to `exp`, `jwt-decoder` makes the `nbf` timestamp easily visible, helping to diagnose why a token might be rejected even if it's not expired. This is crucial for systems that implement delayed token activation. ### Scenario 5: Debugging JWT Issuance Logic on the Server-Side **Problem:** Your server-side code that generates JWTs is not producing valid tokens, leading to client-side authentication failures. **Debugging with `jwt-decoder`:** 1. **Generate a Test Token:** Have your server-side code generate a JWT for a known user/scenario. 2. **Copy the Generated Token:** Get the JWT string directly from your server's output or logs. 3. **Use `jwt-decoder` Locally:** Run the `jwt-decoder` tool on your local machine (or a separate debugging environment) with the *same* secret key or public key that your server is configured to use for signing. 4. **Analyze the Output:** * **Header/Payload Mismatch:** Are the `alg`, `typ`, and claims in the decoded token exactly what your server intended to put in them? * **Incorrect Timestamps:** Are `iat`, `exp`, and `nbf` timestamps being calculated correctly? * **Signature Verification:** Does the signature verify correctly using your local `jwt-decoder` and the server's secret/key? If not, the signing logic on the server is likely flawed. **Insight:** By using `jwt-decoder` to inspect tokens generated by your own server, you can quickly identify bugs in your token generation logic, such as incorrect claim values, faulty timestamp calculations, or improper signing. ### Scenario 6: Mismatched Key/Secret between Token Issuance and Verification **Problem:** Tokens are issued successfully, but verification consistently fails. **Debugging with `jwt-decoder`:** 1. **Obtain Issued JWT:** Get a token generated by your authentication server. 2. **Obtain Signing Key/Secret:** Access the *exact* secret key (for HS algorithms) or public key (for RS algorithms) used by your *authentication server* to sign tokens. 3. **Obtain Verification Key/Secret:** Access the *exact* secret key or public key used by your *resource server* (or API gateway) to verify tokens. 4. **Use `jwt-decoder`:** * If using HS256, use the *verification server's* secret with `jwt-decoder --secret --alg HS256`. * If using RS256, use the *verification server's* public key with `jwt-decoder --publicKey --alg RS256`. 5. **Analyze Results:** * If verification passes with the verification secret/key, but failed previously, it indicates an issue with how the verification secret/key was configured on the server. * If verification fails even with the correct verification secret/key, it strongly suggests that the *issuance* process used a different key/secret than expected, or the token was tampered with. **Insight:** This scenario highlights the importance of consistent key management. `jwt-decoder` helps confirm if the keys used for signing and verification are truly aligned. ## Global Industry Standards and Best Practices The use and debugging of JWTs are governed by RFC specifications and industry best practices. Understanding these ensures your debugging efforts are aligned with robust security principles. ### RFC 7519: JSON Web Token (JWT) This is the foundational RFC for JWTs. It defines the structure (header, payload, signature), registered claims, and the Base64Url encoding. Any JWT decoder must adhere to these specifications for accurate parsing. ### RFC 7518: JSON Web Algorithms (JWA) This RFC specifies the algorithms used to sign and encrypt JWTs. Common algorithms like HS256, RS256, ES256, and PS256 are defined here. A robust JWT decoder should support a wide range of these algorithms. ### RFC 7515: JSON Web Signature (JWS) This RFC defines how to represent signed JSON objects. JWTs are a specific application of JWS. Understanding JWS concepts, like how the signature is generated and verified, is crucial for deep debugging. ### Best Practices for JWT Security and Debugging: * **Use Strong Secrets/Keys:** For symmetric algorithms (HS256), use long, random, and unguessable secrets. For asymmetric algorithms (RS256), use strong private keys. * **Rotate Keys Periodically:** Regularly rotate your signing and verification keys to mitigate the impact of any potential compromise. * **Validate All Claims:** Never trust claims without validation. Always check `exp`, `iss`, `aud`, and any custom claims critical for authorization. * **Keep Tokens Short-Lived:** Issue tokens with short expiration times to limit the window of opportunity for attackers if a token is compromised. Use refresh tokens for longer sessions. * **HTTPS Everywhere:** Always transmit JWTs over HTTPS to prevent interception. * **Don't Store Sensitive Data in JWTs:** JWTs are encoded, not encrypted. Anyone with the token can read its contents. Sensitive information should be handled via other secure channels or encrypted within the JWT if necessary (though this adds complexity and is less common for standard authentication). * **Use a Reputable JWT Library:** When implementing JWT handling in your application, use well-vetted libraries (e.g., `jsonwebtoken` for Node.js, `PyJWT` for Python) that are actively maintained and follow RFC standards. `jwt-decoder` often relies on these underlying libraries. ## Multi-language Code Vault: Implementing JWT Decoding and Verification While `jwt-decoder` is a command-line tool, understanding how to perform these actions programmatically in various languages is essential for integrating debugging into your development workflows. Below are examples using popular libraries. ### Node.js (using `jsonwebtoken`) javascript const jwt = require('jsonwebtoken'); const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; const secretKey = 'your-super-secret-key'; const publicKey = '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----'; // For asymmetric // Basic Decoding (Header and Payload) try { const decoded = jwt.decode(token); console.log('Decoded Header:', decoded.header); console.log('Decoded Payload:', decoded.payload); } catch (error) { console.error('Error decoding token:', error.message); } // Verification (Symmetric - HS256) try { const verified = jwt.verify(token, secretKey, { algorithms: ['HS256'] }); console.log('Token verified successfully (HS256):', verified); } catch (error) { console.error('Token verification failed (HS256):', error.message); // If verification fails, you can still try to decode to inspect claims if needed const decodedForInspection = jwt.decode(token); console.log('Decoded Payload for inspection:', decodedForInspection ? decodedForInspection.payload : 'Could not decode'); } // Verification (Asymmetric - RS256) - Requires a PEM formatted public key // const decodedHeader = jwt.decode(token, { complete: true }).header; // Get algorithm first if not known // try { // const verified = jwt.verify(token, publicKey, { algorithms: [decodedHeader.alg] }); // console.log(`Token verified successfully (${decodedHeader.alg}):`, verified); // } catch (error) { // console.error(`Token verification failed (${decodedHeader.alg}):`, error.message); // } ### Python (using `PyJWT`) python import jwt from datetime import datetime, timezone token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' secret_key = 'your-super-secret-key' public_key = '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----' # For asymmetric # Basic Decoding (Header and Payload) try: decoded_payload = jwt.decode(token, options={"verify_signature": False}) print("Decoded Payload:", decoded_payload) # To get header, we need to decode it manually or use specific options # A simpler way is to split and decode Base64Url parts header_encoded, _, _ = token.split('.') import base64 import json header = json.loads(base64.urlsafe_b64decode(header_encoded + '==').decode('utf-8')) print("Decoded Header:", header) except jwt.ExpiredSignatureError: print("Token is expired") except jwt.InvalidTokenError as e: print("Error decoding token:", e) # Verification (Symmetric - HS256) try: decoded_verified = jwt.decode(token, secret_key, algorithms=['HS256']) print("Token verified successfully (HS256):", decoded_verified) except jwt.ExpiredSignatureError: print("Token is expired") except jwt.InvalidSignatureError: print("Invalid signature (HS256)") # Inspect claims if signature is invalid try: decoded_for_inspection = jwt.decode(token, options={"verify_signature": False}) print("Decoded Payload for inspection:", decoded_for_inspection) except Exception as e: print("Could not decode payload for inspection:", e) except jwt.InvalidTokenError as e: print("Token verification failed (HS256):", e) # Verification (Asymmetric - RS256) # try: # decoded_verified = jwt.decode(token, public_key, algorithms=['RS256']) # print("Token verified successfully (RS256):", decoded_verified) # except jwt.ExpiredSignatureError: # print("Token is expired") # except jwt.InvalidSignatureError: # print("Invalid signature (RS256)") # except jwt.InvalidTokenError as e: # print("Token verification failed (RS256):", e) ### Java (using `jjwt`) java import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import java.security.Key; import java.util.Date; // Example token (replace with your actual token) String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; // For HS256, you'd need the secret key bytes. // For RS256, you'd need the public key. // In a real scenario, these would be loaded securely. Key secretKey = Keys.hmacShaKeyFor("your-super-secret-key".getBytes()); // Basic Decoding (get Claims) try { Claims claims = Jwts.parserBuilder() .setSigningKey(secretKey) // Still need a key for parsing, even if verification is off .build() .parseClaimsJws(token).getBody(); // This will throw if signature is invalid without explicit verification off // To truly just decode without verification, you might need to split manually // and Base64 decode header and payload. Or use a library that supports it. // With jjwt, parsing typically implies verification. // Let's simulate by just showing what we get if verification passes. System.out.println("Decoded Claims: " + claims); } catch (io.jsonwebtoken.security.SignatureException e) { System.err.println("Signature verification failed. Token might be invalid or tampered."); // If signature fails, we can't reliably get claims through standard parse. // For debugging, you'd typically inspect the raw token parts. // Example: Splitting token and Base64 decoding header and payload manually. } catch (io.jsonwebtoken.ExpiredJwtException e) { System.err.println("Token has expired."); System.err.println("Expiration Time: " + new Date(e.getClaims().getExpiration().getTime())); System.err.println("Claims: " + e.getClaims()); } catch (Exception e) { System.err.println("Error decoding token: " + e.getMessage()); } // Verification (HS256) try { Claims verifiedClaims = Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token).getBody(); System.out.println("Token verified successfully (HS256): " + verifiedClaims); System.out.println("Subject: " + verifiedClaims.getSubject()); System.out.println("Issued At: " + new Date(verifiedClaims.getIssuedAt().getTime())); System.out.println("Expiration Time: " + new Date(verifiedClaims.getExpiration().getTime())); } catch (io.jsonwebtoken.security.SignatureException e) { System.err.println("Token verification failed (HS256): Invalid Signature."); } catch (io.jsonwebtoken.ExpiredJwtException e) { System.err.println("Token verification failed (HS256): Token Expired."); System.err.println("Expiration Time: " + new Date(e.getClaims().getExpiration().getTime())); } catch (Exception e) { System.err.println("Token verification failed (HS256): " + e.getMessage()); } These code snippets demonstrate how to programmatically decode and verify JWTs. When debugging, you can use these to build tools or integrate checks directly into your testing frameworks. The key takeaway is that the underlying logic of decoding and verifying is crucial, and `jwt-decoder` provides a convenient way to do this outside of your application code. ## Future Outlook: Advanced Debugging and Security The landscape of authentication and authorization is constantly evolving, and so too will the tools and techniques for debugging. ### AI-Assisted Debugging Future JWT debugging tools might leverage AI to: * **Predict Common Errors:** Analyze token structures and common misconfigurations to flag potential issues before they cause failures. * **Suggest Solutions:** Based on the detected problem (e.g., expired token, incorrect claim), AI could suggest specific configuration changes or code fixes. * **Anomaly Detection:** Identify unusual patterns in token usage or generation that might indicate security breaches or misconfigurations. ### Enhanced Visualization Tools Beyond simple text output, more sophisticated visualizers could: * **Timeline Views:** Show the lifecycle of a token, including issuance, expiration, and any rejections. * **Claim Relationship Mapping:** Visualize how different claims relate to authorization decisions. * **Key Management Dashboards:** Provide a centralized view of signing and verification keys, their rotations, and associated JWTs. ### Integration with Observability Platforms JWT decoding capabilities will likely become more deeply integrated into existing observability platforms (e.g., Datadog, Splunk, ELK Stack). This would allow for: * **Real-time Token Inspection:** Automatically decode and analyze JWTs in logs and traces as they are processed. * **Automated Alerting:** Trigger alerts based on specific JWT validation failures or suspicious claim values. * **Contextual Debugging:** Link JWT inspection directly to other telemetry data (logs, metrics, traces) for a holistic view of an authentication issue. ### Zero-Trust Architectures and Decentralized Identity As organizations move towards Zero-Trust models and explore decentralized identity solutions (e.g., Verifiable Credentials), the complexity of token-based authentication will increase. JWT decoders will remain critical, but their scope might expand to handle: * **More Complex Token Formats:** Supporting emerging standards for credential exchange. * **Auditing of Decentralized Interactions:** Verifying the integrity and authenticity of claims from various decentralized entities. ## Conclusion In conclusion, the question "Does a JWT decoder help in debugging authentication issues?" can be answered with an emphatic **YES**. The `jwt-decoder` tool, and the principles it embodies, are not merely conveniences; they are essential components of a robust authentication debugging toolkit. By providing transparency into the opaque world of JWTs, decoders empower architects, developers, and security professionals to swiftly diagnose and resolve a wide array of authentication problems. From ensuring timely token expiration and accurate claim validation to verifying signature integrity and pinpointing key management discrepancies, the ability to inspect and understand JWTs is fundamental. As the digital landscape continues to evolve, the role of powerful, accessible tools like `jwt-decoder` will only grow in importance, ensuring the security and reliability of our interconnected systems. Mastering the art of JWT decoding is a critical skill for anyone involved in building and maintaining modern, secure applications.