Comment se faire des amis de Telegram Bot avec OpenId Connect

Imaginez une situation: les analystes de Foobar Inc. a mené une étude approfondie de la situation du marché et des processus commerciaux de l'entreprise et est parvenu à la conclusion que pour optimiser les coûts et augmenter considérablement le bénéfice de Foobar, le saignement de nez nécessite un compagnon de bot Telegram qui peut égayer les employés dans les moments difficiles.


Naturellement, Foobar ne peut pas permettre à des concurrents insidieux de profiter de leur savoir-faire en ajoutant simplement leur bot à leurs contacts. Par conséquent, le bot est tenu de parler uniquement avec les employés Foobar qui s'authentifient avec l'authentification unique d'entreprise (SSO) basée sur OpenId Connect.



En théorie


OpenId Connect (OIDC) est un protocole d'authentification basé sur la famille de spécifications OAuth 2.0. Dans ce document, le processus d'authentification peut avoir lieu selon différents scénarios appelés flux, et comprend trois parties:


  • propriĂ©taire de la ressource - utilisateur;
  • client - une application demandant l'authentification;
  • serveur d'autorisation (serveur d'autorisation) - une application qui stocke des informations sur l'utilisateur et est capable de l'authentifier.

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


, , : Telegram- SSO. .


, . OIDC :


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

— HTTP, .


:


Organigramme avec code d'autorisation

  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);
        }
    }
}

!



, , , , . , - , / .


Ces problèmes ont déjà été résolus pour nous dans les clients OAuth plus branchés comme Spring Security et Google OAuth Client . Mais à des fins de démonstration, nous allons bien :)


Toutes les sources peuvent être trouvées sur GitHub .


All Articles