Token login with JWT
With Basic login the password travels in every request. Usually it works differently: the client sends the login and password once to a login address and gets a token, a string that proves who the client is. After that the client sends only the token in the Authorization: Bearer ... header. The most common token format is JWT (JSON Web Token): three parts separated by dots, user data in the middle and the server's signature at the end. Without the secret key a token cannot be forged.
Settings
The micronaut-security-jwt module was already added in the previous lesson. Add two lines to the settings:
# src/main/resources/application.properties
micronaut.security.authentication=bearer
micronaut.security.token.jwt.signatures.secret.generator.secret=${JWT_SECRET:changemechangemechangemechangeme}The first line turns on token mode: a POST /login address appears, and the server accepts the Bearer header. The second sets the signing secret. ${JWT_SECRET:...} means "take it from the JWT_SECRET environment variable, or use the value after the colon if there is none". On a server the secret is set only through the variable.
BlogAuthenticationProvider from the previous lesson stays as it is: /login checks the login and password through it.
Getting a token
curl -X POST http://localhost:8080/login \
-H "Content-Type: application/json" \
-d '{"username":"oleg","password":"changeme"}'{"username":"oleg","roles":["AUTHOR"],"expires_in":3600,"access_token":"eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJibG9nIiwic3ViIjoib2xlZyIsIm5iZiI6MTc5MTEwNDk5OCwiZXhwIjoxNzkxMTA4NTk4LCJpYXQiOjE3OTExMDQ5OTgsInJvbGVzIjpbIkFVVEhPUiJdfQ.wmoVIAAmS-k5aPIKf9RS7Q9FUJEgsG5FVcWJMjojLFc","token_type":"Bearer"}The token is valid for 3600 seconds, one hour. The middle part is plain Base64 and can be read:
{"iss":"blog","sub":"oleg","nbf":1791104998,"exp":1791108598,"iat":1791104998,"roles":["AUTHOR"]}sub is the user name, roles are the roles, exp is the expiry time. There is no password in the token. The server does not store issued tokens: it checks the signature and the expiry.
Requests with a token
Save the token in a variable and pass it in the header:
TOKEN=eyJhbGciOiJIUzI1NiJ9...
curl -X POST http://localhost:8080/articles \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"With JWT","body":"Text"}'{"id":1,"title":"With JWT","body":"Text"}The @Secured rules from the previous lesson work the same way, with roles taken from the token. A /me controller with an Authentication parameter shows {"name":"oleg","roles":["AUTHOR"]}.
Error responses
| Situation | Response |
|---|---|
Wrong password at /login | 401 Credentials Do Not Match |
| No token, but the method requires login | 401 Unauthorized |
| The token is broken or expired | 401 Unauthorized |
| The token is valid, but the role is missing | 403 Forbidden |
Common mistake
The HS256 signing algorithm needs a secret of at least 256 bits, which is 32 characters. With a short secret such as JWT_SECRET=changeme, the application starts, but /login answers 500 with an empty body, and the log says:
WARN i.m.s.t.j.g.JwtTokenGenerator - JOSEException while generating token The secret length must be at least 256 bitsUse a long random string and keep it outside the repository.