Como fazer amigos do Telegram bot com o OpenId Connect

Imagine uma situação: analistas da Foobar Inc. conduziu um estudo aprofundado da situação do mercado e dos processos de negócios da empresa e chegou à conclusão de que, para otimizar custos e aumentar significativamente o lucro de Foobar, a hemorragia nasal requer um companheiro bot do Telegram que pode animar os funcionários em tempos difíceis.


Naturalmente, Foobar não pode permitir que concorrentes insidiosos tirem proveito de seu conhecimento simplesmente adicionando seu bot aos contatos. Portanto, o bot precisa conversar apenas com os funcionários do Foobar que se autenticam com o SSO (Corporate Single Sign-on) baseado no OpenId Connect.



Em teoria


O OpenId Connect (OIDC) é um protocolo de autenticação baseado na família de especificações OAuth 2.0. Nele, o processo de autenticação pode ocorrer de acordo com vários cenários chamados fluxos e inclui três partes:


  • proprietário do recurso - usuário;
  • cliente - um aplicativo que solicita autenticação;
  • servidor de autorização (servidor de autorização) - um aplicativo que armazena informações sobre o usuário e pode autenticá-lo.

JWT- (JSON Web Token). OIDC, , , OIDC.


, , : Telegram- SSO. .


, . OIDC :


  • (authorization code flow),
  • (implicit flow),
  • (hybrid flow),
  • , OAuth 2.0.

— HTTP, .


:


Diagrama de fluxo com código de autorização

  1. .
  2. .
  3. .
  4. (, ).
  5. callback URL .
  6. ID .
  7. .

, , , 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;
    // ...

    // ,       ,
    // inline-   .
    @Override
    public void onUpdateReceived(Update update) {
        if (!update.hasMessage()) {
            log.debug("Update has no message. Skip processing.");
            return;
        }

        // Id Telegram-.
        var userId = update.getMessage().getFrom().getId();
        var chatId = update.getMessage().getChatId();
        //  UserInfo (    ,
        //    )  id Telegram-.
        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) {
        //  URL   
        // (.  2   ).
        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 {
    // OAuth20Service —   ScribeJava.
    //  , ,       .
    private final OAuth20Service oAuthService;
    // UserTrackerStorage —   
    // ( state -> id Telegram-).
    private final UserTrackerStorage userTrackers;
    // TokenStorage — ""   ( , ID  
    //   (refresh token))    id Telegram-.
    //       ,  
    //        , 
    //   .
    //   ,       -,
    //     ,   
    //  ,       ,
    //        .
    //   offline_access.   ,   .
    //  ,  TokenStorage      .
    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) {
        //  ID       UserInfo.
        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) {
        //   OAuth20Service  id ,
        //       , ...
        return new ServiceBuilder(properties.getClientId())
                //  , ...
                .apiSecret(properties.getClientSecret())
                //     (scopes):
                // openid —     OpenId Connect,
                // offline_access —     - 
                //  ,     .
                .defaultScope("openid offline_access")
                //  callback,     
                //     5.
                .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) {
        //         
        // (.  6   ).
        return oidcService.completeAuth(state, code)
                //   ,       .
                .map(userInfo -> Response.temporaryRedirect(botUri).build())
                //    state   ,  
                //         ,
                //  HTTP- 500.
                .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) {
        //  id Telegram-   state.
        return userTrackers.find(state)
                //         id
                // Telegram-.
                .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);
        }
    }
}

!



, , , , . , - , / .


Esses problemas já foram resolvidos para nós em clientes OAuth mais ramificados, como Spring Security e Google OAuth Client . Mas, para fins de demonstração, estamos bem :)


Todas as fontes podem ser encontradas no GitHub .


All Articles