Implémenter OAuth sans bibliothèque client (Java)

Je ne sais pas combien de bières il s'agit, mais ceci est un article sur la façon d'utiliser OAuth. Il existe autant de bibliothèques clientes OAuth en Java qu'il y a d'étoiles, y compris celle de Google, mais ici je vais toucher OAuth sans utiliser la bibliothèque cliente pour l'étude. Je pense qu'il y a beaucoup de bons articles sur le mécanisme d'OAuth lui-même, donc je ne vais pas l'expliquer ici.

Que faire dans cet article

Les références

Using OAuth 2.0 for Web Server Applications OpenID Connect

OAuth

Préparation

Vous devez enregistrer vos informations d'identification OAuth auprès de Google.

Developers Console

Créer des informations d'identification-> ID client OAuth-> Application Web

Le nom est approprié. Ajout de http: // localhost: 8080 / auth à l'URI de redirection approuvé. La valeur ajoutée ici sera la valeur qui peut être utilisée comme destination de redirection de la demande. Cette fois, nous ne l'utiliserons que pour le développement, spécifiez donc localhost. Il ne doit pas être utilisé en production.

Dépendances

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE")
    }
}

apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

dependencies {
    compile(
            'org.springframework.boot:spring-boot-devtools',
            'org.springframework.boot:spring-boot-starter-web',
            'org.springframework.boot:spring-boot-starter-thymeleaf',

            'com.google.guava:guava:25.1-jre',
            'com.google.http-client:google-http-client:1.23.0',
            'com.google.http-client:google-http-client-gson:1.23.0',
            'com.google.code.gson:gson:2.8.5',
    )
}

Tout va bien, mais cette fois, j'utiliserai Spring. ~~ Guava est utilisé pour Base64 et ~~ HttpClient est utilisé pour la simplification des requêtes HTTP. Utilisez Gson pour analyser JSON.

Addendum: Base64 est pris en charge à partir de Java 8 C'est complètement flou.

Écran de connexion

Créez un écran de connexion.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>

<a th:href="${endpoint}">login</a>

</body>
</html>
private static final String clientId = checkNotNull( System.getenv( "CLIENT_ID" ) );

@GetMapping( "/login" )
public String login( Model model ) {

    var endpoint = "https://accounts.google.com/o/oauth2/v2/auth?"
                   + "client_id=" + clientId + "&"
                   + "redirect_uri="
                   + UrlEscapers.urlFormParameterEscaper().escape( "http://localhost:8080/auth" )
                   + "&"
                   + "access_type=" + "online" + "&"
                   + "state=" + "test_state" + "&"
                   + "response_type=" + "code" + "&"
                   + "scope="
                   + UrlEscapers.urlFormParameterEscaper().escape( "email profile" );
    model.addAttribute( "endpoint", endpoint );
    return "login";
}

https: // accounts.google.com / o / oauth2 / v2 / auth est le point de terminaison de l'API Google OAuth. Définissez les informations requises dans le paramètre de requête URL.

paramètre
client_id Un identifiant qui identifie le client. Utilisez la valeur obtenue précédemment dans la console développeur. Ici, il est obtenu à partir de la variable d'environnement.
redirect_uri La destination de la redirection. Utilisez la valeur définie dans la console développeur.
access_type Cette fois, je vais utiliser un navigateur, donconlineSpécifier.
state stateLa valeur elle-même peut être définie librement. La plupartnonceIl est utilisé pour tels que. Cette fois, il s'agit simplement d'une valeur fixe.
response_type Comment recevoir une réponse. Côté serveurcodeC'est réparé.
scope Spécifiez les informations que vous pouvez obtenir avec cette demande. iciemailetprofileEst réglé. Pour les valeurs disponiblesdocumentVoir.

Lorsque l'utilisateur suit le lien généré ici, il sera redirigé vers l'écran de connexion Google. La première fois que vous autorisez l'application à accéder à vos informations utilisateur, une boîte de dialogue de confirmation s'affiche. Lorsque l'utilisateur termine le processus de connexion, il sera redirigé vers redirect_url. Vous pouvez demander un jeton d'accès en utilisant le code inclus dans le paramètre de requête de l'URL.

Obtenir des jetons

@GetMapping( "/auth" )
public String auth( HttpServletRequest request, Model model ) throws IOException {

    var params = parseQueryParams( request );
    checkState( Objects.equals( params.get( "state" ), "test_state" ) );
    var code = URLDecoder.decode( checkNotNull( params.get( "code" ) ), StandardCharsets.UTF_8 );

    // ...

Récupérez le "code" et "l'état" à partir des paramètres de requête. state vérifie que la valeur utilisée dans la requête précédente est renvoyée telle quelle. Notez que vous devrez peut-être décoder l'URL.

var response = httpPost(
        "https://www.googleapis.com/oauth2/v4/token",
        Map.of(
                "code", code,
                "client_id", clientId,
                "client_secret", clientSecret,
                "redirect_uri", "http://localhost:8080/auth",
                "grant_type", "authorization_code"
        )
).parseAsString();
var responseAsMap = parseJson( response );

Envoyez une demande POST à l'URL pour l'acquisition de jetons. C'était la v2 il y a quelque temps, mais pour une raison quelconque, c'est la v4. Je m'en fiche parce que c'est selon le document officiel.

paramètre
code Obtenu ci-dessuscodeEst envoyé tel quel.
client_id Identité du client.
client_secret Secret du client. Comme l'ID, définissez celui spécifié dans la console développeur.
redirect_uri Ce n'est pas une destination de redirection, mais pour vérification. Définissez dans la console et définissez à nouveau la destination de redirection utilisée dans la demande précédente.
grant_type Valeur fixeauthorization_code

Si la demande est reconnue comme réussie, un jeton au format JWT est renvoyé.

Analyse des jetons d'identification

Le jeton Web JSON est divisé en 3 parties, l'en-tête, le corps et la signature séparés par ., alors obtenez chaque partie.

var idTokenList = Splitter.on( "." ).splitToList( idToken );

var header = parseJson( base64ToStr( idTokenList.get( 0 ) ) );
var body = parseJson( base64ToStr( idTokenList.get( 1 ) ) );
var sign = idTokenList.get( 2 );

L'en-tête et les parties du corps sont en JSON encodé en Base64. Si vous ne vérifiez pas le résultat, vous pouvez obtenir la valeur avec juste cela, mais comme c'est un gros problème, vérifions également le jeton.

Obtenez la clé

Obtenez la clé publique Google à utiliser pour la vérification. Obtenez l'emplacement de la clé à partir de https: // accounts.google.com / .well-known / openid-configuration.

var response = httpGet( "https://accounts.google.com/.well-known/openid-configuration" ).parseAsString();
var responseAsMap = parseNestedJson( response );
var jwks = responseAsMap.get( "jwks_uri" ).toString();

Envoyez une requête GET à cet URI pour obtenir le JSON.

var keyResponse = httpGet( jwks ).parseAsString();
var keysResponseObj = parseJsonAs( keyResponse, Keys.class );
return keysResponseObj.keys.stream()
                           .filter( k -> k.kid.equals( kid ) )
                           .findAny()
                           .orElseThrow();

Plusieurs clés de chiffrement sont stockées dans le tableau «keys». Recherchez un kid qui correspond à kid dans l'en-tête du jeton d'identification.

var signature = Signature.getInstance( "SHA256withRSA" );
signature.initVerify( KeyFactory.getInstance( "RSA" ).generatePublic( new RSAPublicKeySpec(
        new BigInteger( 1, base64ToByte( n ) ),
        new BigInteger( 1, base64ToByte( e ) )
) ) );
signature.update( contentToVerify.getBytes( StandardCharsets.UTF_8 ) );
return signature.verify( base64ToByte( sign ) );

Convertissez la clé JSON au format Java et assurez-vous qu'elle est bien connue. En fait, le format de clé doit également être obtenu à partir de JSON, mais ici c'est une valeur fixe de RSA.

Après avoir confirmé la signature, l'étape suivante consiste à vérifier les paramètres.

checkState( Set.of( "https://accounts.google.com", "accounts.google.com" ).contains( body.get( "iss" ) ) );

Émetteur, assurez-vous que «non» est légitime. Pour Google, il s'agit de l'un des paramètres suivants: https: // accounts.google.com, ʻaccounts.google.com`.

checkState( Objects.equals( body.get( "aud" ), clientId ) );

Assurez-vous que l'audience, ʻaud`, correspond à l'ID client.

var now = System.currentTimeMillis();
checkState( now <= Long.parseLong( body.get( "exp" ) ) * 1000 + 1000 );
checkState( now >= Long.parseLong( body.get( "iat" ) ) * 1000 - 1000 );

Enfin, assurez-vous que la clé est dans la date d'expiration et que l'heure de signature est valide. La valeur est en secondes. La raison de l'ajout et de la soustraction de 1000 est d'absorber le décalage avec le serveur.

Obtenir les paramètres

Maintenant que nous avons vérifié que le jeton d'identification est valide, nous obtenons la valeur. Ici, l'adresse e-mail, le nom et l'URL de l'image du visage sont acquis et définis dans le modèle.

model.addAttribute( "email", id.get( "email" ) )
     .addAttribute( "name", id.get( "name" ) )
     .addAttribute( "picture", id.get( "picture" ) );
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Result</title>
</head>
<body>

<img th:src="${picture}">
<p th:text="'Email: ' + ${email}"></p>
<p th:text="'Name:  ' + ${name}"></p>

<a th:href="${back}">back</a>

</body>
</html>

Pour utiliser les différentes API de Google, utilisez ʻaccess_token et refresh_token` inclus dans JWT pour envoyer chaque requête.

Contrôle de fonctionnement

Allez dans localhost: 8080 / login.

1.PNG

2.PNG

Allez sur Google et authentifiez-vous.

3.PNG

J'ai eu l'image du visage, l'adresse e-mail et le nom.

Recommended Posts

Implémenter OAuth sans bibliothèque client (Java)
SELECT des données à l'aide de la bibliothèque cliente avec BigQuery
[Python] Deep Learning: J'ai essayé d'implémenter Deep Learning (DBN, SDA) sans utiliser de bibliothèque.
[GoogleCloudPlatform] Utiliser l'API Google Cloud avec la bibliothèque cliente d'API
Calcul de l'excédent sans utiliser le%
Lire des fichiers sur GCS à l'aide de la bibliothèque cliente Cloud Storage
[Django] Implémenter une fonction de téléchargement de fichier image qui n'utilise pas de modèle
Tri à bulles sans utiliser le tri
Écrivez FizzBuzz sans utiliser "="
Tri rapide sans utiliser le tri
Essayez de créer un réseau de neurones en Python sans utiliser de bibliothèque