Microservices in Practice: Developing Instagram Clone
Part3: Authentication Service
Token based authentication
In Monolithic applications, a user provides username and password, the security module validates user credentials and create a session for the user. A session Id is returned to the client and the client stores the session Id in cookies and send it with each request as an identifier for the user.
This approach making the service statefull has an impact on the scalability because a session should be stored for each user, the more users you’ll have the more memory you’ll need.
Also if you divide the app into multiple services, you’ll need to find a way to share the session between services.
In the token based authentication, authentication service generates the token and send it to the client but it doesn’t store it. The client has to send the token with every request in the Authorization header. This token holds the user’s identity information.
Here, I’m using JWT token or Json Web Token, from now on when I will refer to JWT token as simply token.
JWT token structure
Since the token holds user information, it is encoded and encrypted.
A JWT token would look like below
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZGFtIiwiYXV0aG9yaXRpZXMiOlsiVVNFUiJdLCJpYXQiOjE1OTE2NDQ2NjMsImV4cCI6MTU5MTczMTA2M30.9pR6sK-kJmG6Zm7aWVqNrQy9MZ8ponYKz3Wc3nfKU7MD4oXkifjAEgkB6Uc-Dswp3yjWIiNLrpMd7S9SZ1_D_A
If you notice there are 3 sections in the token separated by dot “.”, these sections are Header.Payload.Signature
- Header: contains metadata like type of the token and the hashing algorithm that is used in Signature section.
{
"typ": "JWT",
"alg": "HS512"
}
- Payload: contains user information and token expiration date.
{
"sub": "adam",
"authorities": [
"USER"
],
"iat": 1591644663,
"exp": 1591731063
}
- Signature: contains the signature of the token creator, to make sure that the token isn’t changed.
HMACSHA512(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
Secret
)
Auth Service responsibilities
- Registering new users.
- Validating user credentials and issue a token.
- Stores and retrieves users profile.
- Manages users.
- Sends a message whenever a user created/updated.
Let’s dive deep into code.
Clone the Auth service from GitHub and let’s walk through it.
MongoDB and Apache Kafka (Docker)
Auth service is using MongoDB as the data store for its users, because of scalability, MongoDB has a better performance since it doesn’t have to deal with the overhead of relational databases and it gives a flexibility to evolve the schema as business needs.
Auth service uses Apache Kafka as a way to notify other services whenever user is created, updated or deleted. We can discuss all the “whys” (why Kafka?, why asynchronous? or why notifying other services?) in the comments.
I will be using docker and docker compose for dependencies like MongoDB, Kafka, neo4j, … etc.
If you don’t know what docker and docker compose are, for now, just think of it as a way to have MongoDB and Kafka on your local machine without having to install them on your machine and follow the below instructions.
- Install docker and docker compose.
- Clone this docker-compose file, comment out Neo4j and Cassandra sections for now.
The file should look like below
Note to change the path “/home/amr/instaMongo” (line 29) to a directory that is on your machine.
3. Navigate to the docker compose file directory and type
docker-compose up
User repository and service
I’m using Spring data MongoDB, all you have to do to provide an interface and the operations (following spring data conventions) and spring will provide the implementations for you.
Nothing fancy here, I think method names are descriptive.
Also, user service is self descriptive, it provides CURD operations for User object (create user, update user, update user profile picture, find user by usernme, … etc). You’ll find it uses UserEventSender to send Kafka messages which I will discuss in the next section.
It is worth to mention InstaUserDetailsService, it is a requirement by Spring Security, you should provide a service that implements UserDetailsService and return UserDetails. I will talk about spring security configurations in a later section.
One last thing to note here, annotating the Application class with @EnableMongoAuditing.
This will make spring to set the below properties in the User model.
@CreatedDate
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;
Messaging (Kafka messages)
I’m using Spring Could Stream which ease the way to publish and listen to events (messages).
I provide an interface called UserEventStream (It is a better to name it UserEventChannel) as below
Notice the annotation @Output which mean this channel will be used for publishing messages to it. @Input on the other hand means you’ll listen to it.
Now you need to configure you Kafka and map channels to topics.
Let’s look at Kafka configurations in application.yml
The 1nd part configures Kafka brokers (you can think of it as host or read more about Apache Kafka). Since we are deploying locally, the broker is on localhost and 9092 is the default port.
The 2nd part is what maps momentsUserChanged channel to moments.user.changed topic.
Spring cloud stream will automatically create the topic for you if it is not exist.
Finally, UserEventSender uses UserEventStream to send UserCreated and UserUpdated events to moments.user.changed topic.
Don’t forget to annotate the Application class with @EnableBinding as below.
Spring Security Configuration
Let’s look into SecurityCredentialsConfig.
The first thing is to configure the AuthenticationManager to authenticate users from MongoDB.
Line 11, sets the UserDetailsSevice to our userDetailsService that I explained in the previous section. When a user provide a username and password, spring will call loadUserByUsername from userDetailsService and compare passwords if user exists.
In Line 5, it is a hack I did to add a credentials for Service user for service to service communications. Service still need a token to access other services. I add service account in memory as you can see.
So, when you provide username and password, spring will first look into the in memory auth, if not found will look into the DB auth.
The 2nd to look into in SecurityCredentialsConfig is the below method.
Line 4, set the session creation to stateless, so spring won’t save sessions for users.
Line 7, In case of authentication error send an unauthorized http response (401).
Line 9, set the token authentication filter, which will intercept the request and check if the token is in header and validate the token, we will discuss this in details below.
The remaining part secure the access to all service resources except “/signin” and “/users” for POST method, because you want anyone to be able to login and register.
Now, let’s look into JwtTokenAuthenticationFilter.
Line 5–10, gets the authentication header and check if the token is in the header, if not it will pass to the next filter because a user might be accessing unsecured resource like “/signin” or “/users”.
Line 12, tokenProviderService validate the token and if the token is valid it will create a security context for the user depending on if it is a Service user or normal User.
TokenProviderService is responsible for generating and validating user access token.
Auth service endpoints
ooking into UserEndpoint controller which provides an endpoint for authentication and user manipulation.
POST /signin // validate username and password then return a token
POST /users // creates new user
PUT /users/me/picture // updates current user profile pic
GET /users/{username} // retrieve user by username
GET /users // finds all users
GET /users/me //retrieve logged in user
GET /users/summary/{username} // return only username, name, email a
and profile picture for the user.
GET /users/summary/in" // return uses summaries for the specified usernames
Let’s look into “/signin” response because others are just straight forward.
{
"accessToken":"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZGFtIiwiYXV0aG9yaXRpZXMiOlsiVVNFUiJdLCJpYXQiOjE1OTE2NDQ2NjMsImV4cCI6MTU5MTczMTA2M30.9pR6sK-kJmG6Zm7aWVqNrQy9MZ8ponYKz3Wc3nfKU7MD4oXkifjAEgkB6Uc-Dswp3yjWIiNLrpMd7S9SZ1_D_A",
"tokenType": "Bearer"
}
It returns a token and the client has to save it somewhere and send it with every request.
Service Discovery Registration
Finally, the service need to register itself in the service discovery, so other services can find it by its name (spring application name), in our case it is “insta-auth”.
If you don’t know what are we talking about, then check part 2 of this series.
To register add the below configurations in application.yml
Now, run Mongo, kafka, insta-discovery and Auth service and play with it.
Open
http://localhost:8761
You should see insta-auth registered, try to create user, login, .. etc.
If you can’t see the service registered in the discovery service or you don’t know how to add the token in the header, please leave a comment to troubleshoot it together.
About the Creator
Amr Khaled
I’m a software engineer who is passionate about software architecture and design. Enjoy coding in Java, Scala, and JavaScript.
Comments
There are no comments for this story
Be the first to respond and start the conversation.