Imagine una situación: analistas de Foobar Inc. realizó un estudio exhaustivo de la situación del mercado y los procesos comerciales de la empresa y llegó a la conclusión de que para optimizar los costos y aumentar significativamente las ganancias de Foobar, la hemorragia nasal requiere un compañero de bot de Telegram que pueda animar a los empleados en tiempos difíciles.
Naturalmente, Foobar no puede permitir que competidores insidiosos aprovechen sus conocimientos simplemente agregando su bot a sus contactos. Por lo tanto, el bot debe hablar solo con los empleados de Foobar que se autentican con el inicio de sesión único corporativo (SSO) basado en OpenId Connect.
En teoria
OpenId Connect (OIDC) es un protocolo de autenticación basado en la familia de especificaciones OAuth 2.0. En él, el proceso de autenticación puede llevarse a cabo de acuerdo con varios escenarios llamados flujos, e incluye tres partes:
- propietario del recurso - usuario;
- cliente: una aplicación que solicita autenticación;
- servidor de autorización (servidor de autorización): una aplicación que almacena información sobre el usuario y puede autenticarlo.
JWT- (JSON Web Token). OIDC, , , OIDC.
, , : Telegram- SSO. .
, . OIDC :
- (authorization code flow),
- (implicit flow),
- (hybrid flow),
- , OAuth 2.0.
— HTTP, .
:
- .
- .
- .
- (, ).
- callback URL .
- ID .
- .
, , , ID , , Telegram- .
state , (XSRF). state 2 URL , 5 callback URL . , 2 state id Telegram-, 7 state id Telegram- . !
, , :
- Keycloak — open source (Identity and Access Management), OAuth 2.0, Open ID Connect SAML.
- Spring Boot .
- TelegramBots — Java Telegram-. — Spring Boot.
- ScribeJava — OAuth Java. , OAuth , Keycloak. , Keycloak - , Spring Boot — . Keycloak , — Telegram-, state id Telegram-, .
- Java JWT Auth0 — JWT Java, ID .
:
@Component
public class Bot extends TelegramLongPollingBot {
private final OidcService oidcService;
@Override
public void onUpdateReceived(Update update) {
if (!update.hasMessage()) {
log.debug("Update has no message. Skip processing.");
return;
}
var userId = update.getMessage().getFrom().getId();
var chatId = update.getMessage().getChatId();
oidcService.findUserInfo(userId).ifPresentOrElse(
userInfo -> greet(userInfo, chatId),
() -> askForLogin(userId, chatId));
}
private void greet(UserInfo userInfo, Long chatId) {
var username = userInfo.getPreferredUsername();
var message = String.format(
"Hello, <b>%s</b>!\nYou are the best! Have a nice day!",
username);
sendHtmlMessage(message, chatId);
}
private void askForLogin(Integer userId, Long chatId) {
var url = oidcService.getAuthUrl(userId);
var message = String.format("Please, <a href=\"%s\">log in</a>.", url);
sendHtmlMessage(message, chatId);
}
}
— UserInfo id Telegram- URL :
@Service
public class OidcService {
private final OAuth20Service oAuthService;
private final UserTrackerStorage userTrackers;
private final TokenStorage accessTokens;
public Optional<UserInfo> findUserInfo(Integer userId) {
return accessTokens.find(userId)
.map(UserInfo::of);
}
public String getAuthUrl(Integer userId) {
var state = UUID.randomUUID().toString();
userTrackers.put(state, userId);
return oAuthService.getAuthorizationUrl(state);
}
}
, , :
public class UserInfo {
private final String subject;
private final String preferredUsername;
static UserInfo of(OpenIdOAuth2AccessToken token) {
var jwt = JWT.decode(token.getOpenIdToken());
var subject = jwt.getSubject();
var preferredUsername = jwt.getClaim("preferred_username").asString();
return new UserInfo(subject, preferredUsername);
}
}
OAuth20Service:
@Configuration
@EnableConfigurationProperties(OidcProperties.class)
class OidcAutoConfiguration {
@Bean
OAuth20Service oAuthService(OidcProperties properties) {
return new ServiceBuilder(properties.getClientId())
.apiSecret(properties.getClientSecret())
.defaultScope("openid offline_access")
.callback(properties.getCallback())
.build(KeycloakApi.instance(properties.getBaseUrl(), properties.getRealm()));
}
}
Callback endpoint :
@Component
@Path("/auth")
public class AuthEndpoint {
private final URI botUri;
private final OidcService oidcService;
@GET
@Produces("text/plain; charset=UTF-8")
public Response auth(
@QueryParam("state") String state,
@QueryParam("code") String code) {
return oidcService.completeAuth(state, code)
.map(userInfo -> Response.temporaryRedirect(botUri).build())
.orElseGet(() -> Response.serverError().entity("Cannot complete authentication").build());
}
}
OidcService, completeAuth:
@Service
public class OidcService {
private final OAuth20Service oAuthService;
private final UserTrackerStorage userTrackers;
private final TokenStorage accessTokens;
public Optional<UserInfo> completeAuth(String state, String code) {
return userTrackers.find(state)
.map(userId -> requestAndStoreToken(code, userId))
.map(UserInfo::of);
}
private OpenIdOAuth2AccessToken requestAndStoreToken(
String code,
Integer userId) {
var token = requestToken(code);
accessTokens.put(userId, token);
return token;
}
private OpenIdOAuth2AccessToken requestToken(String code) {
try {
return (OpenIdOAuth2AccessToken) oAuthService.getAccessToken(code);
} catch (IOException | InterruptedException | ExecutionException e) {
throw new RuntimeException("Cannot get access token", e);
}
}
}
!
, , , , . , - , / .
Estos problemas ya se han resuelto para nosotros en clientes OAuth más ramificados como Spring Security y Google OAuth Client . Pero para fines de demostración, estamos bien :)
Todas las fuentes se pueden encontrar en GitHub .