So machen Sie mit OpenId Connect Telegramm-Bot-Freunde

Stellen Sie sich eine Situation vor: Analysten von Foobar Inc. fĂŒhrte eine grĂŒndliche Untersuchung der Marktsituation und der GeschĂ€ftsprozesse des Unternehmens durch und kam zu dem Schluss, dass Nasenbluten einen Telegramm-Bot-Begleiter benötigt, der die Mitarbeiter in schwierigen Zeiten aufmuntern kann, um die Kosten zu optimieren und den Gewinn von Foobar signifikant zu steigern.


NatĂŒrlich kann Foobar nicht zulassen, dass heimtĂŒckische Konkurrenten ihr Know-how nutzen, indem sie einfach ihren Bot zu ihren Kontakten hinzufĂŒgen. Daher muss der Bot nur mit Foobar-Mitarbeitern sprechen, die sich mit dem auf OpenId Connect basierenden Corporate Single Sign-On (SSO) authentifizieren.



In der Theorie


OpenId Connect (OIDC) ist ein Authentifizierungsprotokoll, das auf der OAuth 2.0-Spezifikationsfamilie basiert. Darin kann der Authentifizierungsprozess gemĂ€ĂŸ verschiedenen Szenarien stattfinden, die als Flows bezeichnet werden, und umfasst drei Parteien:


  • Ressourcenbesitzer - Benutzer;
  • Client - eine Anwendung, die eine Authentifizierung anfordert;
  • Autorisierungsserver (Autorisierungsserver) - Eine Anwendung, die Informationen ĂŒber den Benutzer speichert und ihn authentifizieren kann.

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


, , : Telegram- SSO. .


, . OIDC :


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

— HTTP, .


:


Flussdiagramm mit Autorisierungscode

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

!



, , , , . , - , / .


Diese Probleme wurden fĂŒr uns bereits in verzweigteren OAuth-Clients wie Spring Security und Google OAuth Client behoben . Aber zu Demonstrationszwecken sind wir in Ordnung :)


Alle Quellen finden Sie auf GitHub .


All Articles