OpenId Connect рдХреЗ рд╕рд╛рде Telegram bot рджреЛрд╕реНрдд рдХреИрд╕реЗ рдмрдирд╛рдпреЗрдВ

рдПрдХ рд╕реНрдерд┐рддрд┐ рдХреА рдХрд▓реНрдкрдирд╛ рдХрд░реЗрдВ: Foobar Inc. рдХреЗ рд╡рд┐рд╢реНрд▓реЗрд╖рдХ рдХрдВрдкрдиреА рдХреА рдмрд╛рдЬрд╛рд░ рдХреА рд╕реНрдерд┐рддрд┐ рдФрд░ рд╡реНрдпрд╛рд╡рд╕рд╛рдпрд┐рдХ рдкреНрд░рдХреНрд░рд┐рдпрд╛рдУрдВ рдХрд╛ рдЧрд╣рди рдЕрдзреНрдпрдпрди рдХрд┐рдпрд╛ рдФрд░ рдЗрд╕ рдирд┐рд╖реНрдХрд░реНрд╖ рдкрд░ рдкрд╣реБрдВрдЪреЗ рдХрд┐ рд▓рд╛рдЧрддреЛрдВ рдХрд╛ рдЕрдиреБрдХреВрд▓рди рдХрд░рдиреЗ рдФрд░ рдлрд╝реЙрдмрд░ рдХреЗ рд▓рд╛рдн рдХреЛ рдмрдврд╝рд╛рдиреЗ рдХреЗ рд▓рд┐рдП, рдирдХрд╝рд▓ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рдЯреЗрд▓реАрдЧреНрд░рд╛рдо рдмреЙрдЯ рд╕рд╛рдереА рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрддреА рд╣реИ рдЬреЛ рдореБрд╢реНрдХрд┐рд▓ рд╕рдордп рдореЗрдВ рдХрд░реНрдордЪрд╛рд░рд┐рдпреЛрдВ рдХреЛ рдЦреБрд╢ рдХрд░ рд╕рдХрддрд╛ рд╣реИред


рд╕реНрд╡рд╛рднрд╛рд╡рд┐рдХ рд░реВрдк рд╕реЗ, рдлрд╝реЛрдмрд╛рд░ рдХрдкрдЯреА рдкреНрд░рддрд┐рдпреЛрдЧрд┐рдпреЛрдВ рдХреЛ рдЕрдкрдиреЗ рд╕рдВрдкрд░реНрдХреЛрдВ рдореЗрдВ рдХреЗрд╡рд▓ рдЕрдкрдиреЗ рдмреЙрдЯ рдХреЛ рдЬреЛрдбрд╝рдХрд░ рдЙрдирдХреА рдЬрд╛рдирдХрд╛рд░реА рдХрд╛ рд▓рд╛рдн рдЙрдард╛рдиреЗ рдХреА рдЕрдиреБрдорддрд┐ рдирд╣реАрдВ рджреЗ рд╕рдХрддрд╛ рд╣реИред рдЗрд╕рд▓рд┐рдП, рдмреЙрдЯ рдХреЛ рдХреЗрд╡рд▓ рдлрд╝реЛрдмрд╛рд░ рдХрд░реНрдордЪрд╛рд░рд┐рдпреЛрдВ рдХреЗ рд╕рд╛рде рдмрд╛рдд рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрддреА рд╣реИ рдЬреЛ рдУрдкрдирдЖрдИрдб рдХрдиреЗрдХреНрдЯ рдкрд░ рдЖрдзрд╛рд░рд┐рдд рдХреЙрд░реНрдкреЛрд░реЗрдЯ рд╕рд┐рдВрдЧрд▓ рд╕рд╛рдЗрди-рдСрди (рдПрд╕рдПрд╕рдУ) рдХреЗ рд╕рд╛рде рдкреНрд░рдорд╛рдгрд┐рдд рдХрд░рддреЗ рд╣реИрдВред



рд╕рд┐рджреНрдзрд╛рдВрдд рд░реВрдк рдореЗрдВ


OpenId рдХрдиреЗрдХреНрдЯ (OIDC) OAuth 2.0 рд╡рд┐рдирд┐рд░реНрджреЗрд╢ рдкрд░рд┐рд╡рд╛рд░ рдкрд░ рдЖрдзрд╛рд░рд┐рдд рдПрдХ рдкреНрд░рдорд╛рдгреАрдХрд░рдг рдкреНрд░реЛрдЯреЛрдХреЙрд▓ рд╣реИред рдЗрд╕рдореЗрдВ, рдкреНрд░рдорд╛рдгреАрдХрд░рдг рдкреНрд░рдХреНрд░рд┐рдпрд╛ рд╡рд┐рднрд┐рдиреНрди рдкрд░рд┐рджреГрд╢реНрдпреЛрдВ рд╕реЗ рдЧреБрдЬрд░ рд╕рдХрддреА рд╣реИ рдЬрд┐рд╕реЗ рдкреНрд░рд╡рд╛рд╣ рдХрд╣рд╛ рдЬрд╛рддрд╛ рд╣реИ, рдФрд░ рдЗрд╕рдореЗрдВ рддреАрди рдкрдХреНрд╖ рд╢рд╛рдорд┐рд▓ рд╣реИрдВ:


  • рд╕рдВрд╕рд╛рдзрди рд╕реНрд╡рд╛рдореА - рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛;
  • рдЧреНрд░рд╛рд╣рдХ - рдПрдХ рдЖрд╡реЗрджрди рдЬреЛ рдкреНрд░рдорд╛рдгреАрдХрд░рдг рдХрд╛ рдЕрдиреБрд░реЛрдз рдХрд░рддрд╛ рд╣реИ;
  • рдкреНрд░рд╛рдзрд┐рдХрд░рдг рд╕рд░реНрд╡рд░ (рдкреНрд░рд╛рдзрд┐рдХрд░рдг рд╕рд░реНрд╡рд░) - рдПрдХ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдЬреЛ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЬрд╛рдирдХрд╛рд░реА рд╕рдВрдЧреНрд░рд╣реАрдд рдХрд░рддрд╛ рд╣реИ рдФрд░ рдЙрд╕реЗ рдкреНрд░рдорд╛рдгрд┐рдд рдХрд░рдиреЗ рдореЗрдВ рд╕рдХреНрд╖рдо рд╣реЛрддрд╛ рд╣реИред

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


, , : Telegram- SSO. .


, . OIDC :


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

тАФ HTTP, .


:


рдкреНрд░рд╛рдзрд┐рдХрд░рдг рдХреЛрдб рдХреЗ рд╕рд╛рде рдкреНрд░рд╡рд╛рд╣ рдЖрд░реЗрдЦ

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

!



, , , , . , - , / .


рд╕реНрдкреНрд░рд┐рдВрдЧ рд╕рд┐рдХреНрдпреЛрд░рд┐рдЯреА рдФрд░ Google OAuth рдХреНрд▓рд╛рдЗрдВрдЯ рдЬреИрд╕реЗ рдЕрдзрд┐рдХ рдмреНрд░рд╛рдВрдЪ рд╡рд╛рд▓реЗ OAuth рдХреНрд▓рд╛рдЗрдВрдЯ рдореЗрдВ рдпреЗ рд╕рдорд╕реНрдпрд╛рдПрдБ рдкрд╣рд▓реЗ рд╣реА рд╣рд▓ рд╣реЛ рдЪреБрдХреА рд╣реИрдВ ред рд▓реЗрдХрд┐рди рдкреНрд░рджрд░реНрд╢рди рдкреНрд░рдпреЛрдЬрдиреЛрдВ рдХреЗ рд▓рд┐рдП, рд╣рдо рдареАрдХ рд╣реИрдВ :)


рд╕рднреА рд╕реНрд░реЛрддреЛрдВ GitHub рдкрд░ рдкрд╛рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ ред


All Articles