SSO-Authentication using instant messenger chatbots (account?linking)
Ilya Kovalkov
Software engineer | Java & Kotlin Developer | Proficient in Spring Framework | Interest and some experience in architecture
Imagine you have a chatbot integrated into a messaging platform that serves as one of your communication channels with customers. Let’s say it’s a customer support bot. At some point, to resolve a user's issue, you might need access to sensitive user data. For instance, in the e-commerce sector, this could include information about orders or payments.
In such cases, having a Single Sign-On (SSO) system, enabling user authentication across various usage channels, can be very beneficial. However, I will provide an example where having an SSO is not crucial. For this demonstration, we'll use the OpenID Connect protocol and any third-party service supporting it (such as Google, Facebook, GitHub, or AppleID). Keycloak will be used here as an instance in Docker for demonstration purposes.
As an instant messenger I’ll use Telegram, since it is the easiest to create a new chatbot, and it also has a convenient SDK for working with chatbot functions.
Let's consider a setup with the following components:
Components in the Diagram:
For this setup, we assume the chatbot backend maintains internal sessions for each user to avoid processing each message in isolation. These sessions allow the backend to recognize the user, maintain some status, and perform authorized requests on their behalf.
The user login or registration process with subsequent authentication can be schematically illustrated as follows:
This diagram does not specify the particular OAuth 2 flow used. It simply shows that the response with token sets is received from the OAuth2 Server by the Backend and stored in a session created for the user (identified by ChatID). It’s important to distinguish between an internal chat-session, created for a specific chat/dialog, and a web-session created upon authentication via a link, tied directly to the browser.
The chat-session might be initiated upon the first authentication request. Within the chat-session, we need to introduce the concepts of session and Authenticated status for the user.
Thus, user authentication for accessing data via the messenger happens at the backend level, utilizing the user's current chat-session. The backend is responsible for access control to user sessions, and its implementation depends on how the backend interacts with the chatbot.
When a user requests to view their orders, for example, the process diagram looks as follows:
Since my primary tool is the Spring Framework and its components, I chose it to develop such a bot. Spring Security and Spring OAuth 2.0 Client offer the necessary functionality out-of-the-box for web-session support. However, Spring does not manage our internal chat-sessions, so additional configuration is required.
Step-by-Step Configuration:
领英推荐
Step 1: Creating a Service to Manage Chat Sessions?
In the simplest form, using an in-memory cache without considering various session handling aspects, a minimally viable service can look like this:
@Service
class SessionService {
private val sessions = mutableMapOf<String, Authentication>()
fun saveChatAuthSession(it: String, authentication: Authentication) {
sessions[it] = authentication
}
fun getChatAuthSession(it: String): Authentication? = sessions[it]
}
A reasonable question here is why the Authentication type is used, which only contains the ID-token, but not the access or refresh tokens. It's possible to intercept the OAuth2LoginAuthenticationToken during the OAuth2 service response handling, but it's unnecessary since Spring saves an OAuth2AuthorizedClient object in OAuth2AuthorizedClientRepository, containing the required data. We can always retrieve the relevant OAuth2AuthorizedClient using the OAuth2 client ID and the Authentication instance or just the principal name.
Step 2: Capturing the Unique ChatID
This can be approached in several ways. A logical option is to use a URI to initiate the login process and pass the ChatID as a query parameter. For example: http(s)://mydomain.com/login?chatId=123456 We can then retrieve chatId either by creating a controller for the login endpoint or using an additional filter (OncePerRequestFilter). The idea is to temporarily store this value in the web session and retrieve it later when we get the tokens from our OAuth2 Authorization Server.
A following controller can be implemented for this demo:
@Controller
class TgChatAuthController {
@RequestMapping("/login")
fun index(@RequestParam(CHAT_ID_SESSION_PARAMETER) chatId: String, session: HttpSession): String {
session.setAttribute(CHAT_ID_SESSION_PARAMETER, chatId)
return "redirect:/oauth2/authorization/keycloak"
}
}
Step 3: Associating Authentication with ChatID?
In Step 1, I explained why it's sufficient to associate ChatID with Authentication without handling the tokens directly. There are several ways to access Authentication. The most obvious one is via SecurityContextHolder.getContext().authentication.
We need to determine where in the code to make this call. For instance, we could use an intermediate endpoint in defaultSuccessUrl configuration. However, OAuth2 login already involves multiple redirects. Adding another would be unnecessary.
For this demo, I will assign a custom successHandler, extending the method onAuthenticationSuccess(...) of the SavedRequestAwareAuthenticationSuccessHandler class. The only added action is retrieving the ChatID from the session, which we stored in the previous step, and saving the Authentication object in our sessionService.
@Bean
@Throws(Exception::class)
fun web(http: HttpSecurity): SecurityFilterChain {
return http
.oauth2Login { oauth2Login ->
oauth2Login.successHandler(sessionSavingSuccessHandler())
}
.build()
}
private fun sessionSavingSuccessHandler(): SavedRequestAwareAuthenticationSuccessHandler {
val successHandler = object : SavedRequestAwareAuthenticationSuccessHandler() {
override fun onAuthenticationSuccess(
request: HttpServletRequest,
response: HttpServletResponse,
authentication: Authentication
) {
(request.session.getAttribute(CHAT_ID_SESSION_PARAMETER) as String).let {
sessionService.saveChatAuthSession(it, authentication)
}
super.onAuthenticationSuccess(request, response, authentication)
}
}
successHandler.setDefaultTargetUrl("https://t.me/${botId}")
successHandler.setAlwaysUseDefaultTargetUrl(true)
return successHandler
}
Step 4: Making Authorized Requests to the Resource Server?
Now that we have the necessary tokens (at least an access_token) and can associate them with a user's chat, we can make authorized requests to the Resource Server to retrieve protected data.
The Spring Security documentation provides several examples and approaches for implementing the client-side. To verify the approach, a simple method can be used, forcibly sending the BearerToken in the header:
@Bean
fun resourceServerClient(): RestTemplate {
val restTemplate = RestTemplateBuilder().rootUri(serverUrl + RESOURCE_SERVER_SUFFIX).build()
restTemplate.interceptors.add(ClientHttpRequestInterceptor { request, body, execution ->
request.headers[CHAT_ID_HEADER]?.first()?.let { chatId ->
sessionService.getChatAuthSession(chatId)?.let { principal ->
val authorizedClient =
authorizedClientService.loadAuthorizedClient<OAuth2AuthorizedClient>("keycloak", principal.name)
request.headers.setBearerAuth(authorizedClient.accessToken.tokenValue)
}
}
execution.execute(request, body)
})
return restTemplate
}
This method is obviously not optimal and has several drawbacks, such as not checking the access_token lifetime and validity. However, it demonstrates the basics of such interactions.
In future articles or as a continuation of this one, I will share more information on managing tokens and calling services with protected data. Although these answers are already provided in Spring documentation, this example offers a clear understanding of using OAuth2 tools and authorizing requests to the Resource Server from any chatbot.
You can find the complete code example in my GitHub repository.
Coaching Software Devs to get Clients on LinkedIn | Get direct clients without recruiters even in bad markets | Expert Client Acquisition Training | DM me ?clients“ to get started
3 个月Nice article. And very detailed step-by-step walkthrough, Ilya ??