The labs on this site need JavaScript to run. The same content is written out below.
Fundamentals › Security
What is a token, and what does the server look at to trust it?
When a login succeeds, the server writes down "this person is kim" as text, attaches a signature calculated with a secret key that only the server knows, and gives it to the client. This text is called a token, and the most common format is the JWT. A token is three pieces joined by dots (.): the header, the payload, and the signature. The client holds the token and sends it in the Authorization: Bearer token header with every request. The server does not keep a separate list of logged-in people. Instead, it recalculates the signature from the header and the payload of the token it received, and checks whether it is the same as the signature that came with the token. If it is the same, the server trusts that the token was made by the server and has not been changed, and reads the payload. In this lab, you change the payload of the token, remove the signature, sign with a different key, or take the token away, and also make the server skip the signature check, and you see whom the server trusts and what response it gives. The code panel shows the request the client sends and the token decoded, and it is not Java code. The signature is long, so it is written as (signature). The server in this lab is built to answer a GET /me request with "Hello, user name", and its server code is not shown on the screen.
Checked against real execution · Sent 10 combinations of tokens as real HTTP requests to an http server on Node.js 24.18.0 and checked against the results. The implementation that makes the tokens was checked against the example in RFC 7515 Appendix A.1 and the verification of the JWT library jose4j 0.9.5
GET /me HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoia2ltIn0.(signature)
# Split the token at the dots (.) and decode it
header: {"alg":"HS256"}
payload: {"user":"kim"}
signature: (value calculated from the header and payload with the server's secret key)
# Server
signature check: yes
The server is a single one started with the http module of Node.js 24.18.0 on a free port of 127.0.0.1. POST /login makes a token from the name it receives and returns it (the password check is left out and the server stores nothing). GET /me takes the token after Bearer from the Authorization header and splits it at the dots. If signature verification is on, it recalculates HMAC-SHA256 from the header and the payload and compares it with the signature that came with the token (if it does not match or is empty, the result is 401). If verification is off, it does not compare, reads the user in the payload, and answers with 200 and "Hello, user name". This decision is made by one function that reads only the request, the key, and whether verification is on, and it does not look up any storage such as a list of logged-in people. The client sent real HTTP requests with Node.js http.request, and in all 10 combinations we checked the first line of the request that the server received (GET /me, HTTP/1.1), the Authorization header, the result of the signature check, and the response status and body, and checked them against the model. The token line in the code panel is the same text as the header the server received, with only the signature part shortened to (signature), and the decoded header and payload are the same text as the token the server received decoded with base64url. The request the server received also had other headers such as Host and Connection, and they are shortened on the screen. The localhost on the screen was not used, and the request went to another port on the PC used to write this lab. That the implementation that makes the tokens follows the standard was checked first by running the same calculation on the example in RFC 7515 Appendix A.1 (the key, header, and payload written in the document) and getting the same value as the signature in the document. We also verified the same tokens with an HS256 key with jose4j 0.9.5, a JWT library in the Gradle cache on the PC used to write this lab. Only the token the server gave was valid, and the one with the payload changed, the one with the signature removed, and the one signed with a different key were not valid, which was the same as the decision of this server. Also, when we sent the token the server gave to a new server (with the same key) that had never received a login, it accepted it with 200, and a server with a different key rejected the same token. When the payload was changed to admin, the value the server recalculated from that payload differed from the signature that came with the token, and the signature made with a different key also differed from the value the server calculated. The token with the signature removed was sent in the form "header.payload.", with the dot left and only the place for the signature emptied. We separately saw that "header.payload", with the dot removed too, becomes 401 as a malformed token on this server whether or not signature verification is on, but this comes from the way this server was built, so it is not used on the screen. The 401 response had a WWW-Authenticate: Bearer header, and it is shortened on the screen. This lab deals only with a token with the signature part emptied, and does not deal with the attack of changing alg in the header to none. This server does not read alg in the header and always verifies with HMAC-SHA256, and the problems around algorithms are covered in the in-depth lab "JWT structure and verification". It also does not cover the expiration time of a token (exp), problems when a token is stolen, the difficulty of invalidating a token that was already given, where to keep a token, and the public-key method (RS256). What we opened in the documents and checked is as follows. RFC 7519 sections 1 and 3 (a JWT represents claims as a JSON object in a JWS or JWE structure, and in a signed JWT the claims set is the payload of the JWS), the first step of section 7.2 (a JWT must contain at least one dot, and what is before the first dot is the header), RFC 7515 section 2 (the signing input is the base64url header, a dot, and the base64url payload), section 3 (JWS overview), section 5.2 (signature verification is done on the signing input, and if no verification succeeds, the JWS is invalid), section 7.1 (the compact form joins the header, the payload, and the signature with dots), Appendix A.1 (the HMAC SHA-256 example), RFC 6750 section 1.2 (a Bearer token is a token that anyone who holds it can use), section 2.1 (the token is sent in the Authorization header with the Bearer method), and section 3 (the WWW-Authenticate response header, with the method name Bearer). RFC 6750 is a document for OAuth 2.0 access tokens, but the token in this lab was also sent in a header of the same form. The key and the tokens on the screen are values made for this lab, and the value of the key is not shown on the screen. We did not run it in a browser.
Struck-through text was removed from the first version of the code; highlighted text was added.