Article
· Jan 15 10m de lecture

Authentification JWT

Quand j'étais plus jeune (le détail de mon âge exact ne relève pas du sujet de cet article), le mot "jeton" était synonyme de plaisir. En effet, plusieurs fois par an, j'avais la chance d'aller dans une salle d'arcade pour jouer à des jeux vidéo amusants avec mes amis.

De nos jours, les jetons sont synonymes de sécurité. L'authentification JSON Web Token (JWT) est devenue l'une des normes les plus populaires pour sécuriser les API REST. Heureusement pour les utilisateurs d'IRIS, nous disposons d'un moyen simple de configurer les applications afin qu'elles soient protégées de cette manière. Pourtant, le principe reste similaire à celui de mes anciennes salles d'arcade. Si vous souhaitez jouer à des jeux, vous devez d'abord obtenir des jetons!

La configuration

Notre première tâche consistera à préparer IRIS pour permettre aux utilisateurs d'acquérir des jetons. Nous commencerons par autoriser le système d'authentification JWT à l'échelle du système. Pour ce faire, nous ouvrirons notre portail de gestion du système et nous nous connecterons. Ensuite, nous irons dans Administration du système &gt Sécurité &gt Sécurité du système &gt Authentification\Options de session Web. Il s'agit du même endroit où nous autorisons d'autres alternatives d'authentification, par exemple LDAP et l'authentification à deux facteurs. Vers le bas de la liste des variantes, nous trouverons un champ appelé "Champ émetteur JWT" (JWT Issuer Field) dans lequel nous devrons saisir une valeur pour identifier l'émetteur du jeton. Il peut s'agir de n'importe quelle chaîne unique, mais il s'agit souvent d'une URL ou d'un domaine. Elle doit être convenue au préalable par les développeurs de l'API et du front-end. Vous pouvez sélectionner l'URL à laquelle les utilisateurs enverront leurs requêtes lorsqu'ils accéderont à l'API. Dans mes exemples, j'opterai pour www.myurl.com.

Ensuite, nous devrons configurer une application Web dans notre portail de gestion du système. Pour ce faire, nous retournerons d'abord dans Administration du système > Sécurité > Applications > Applications Web. Ensuite, nous sélectionnerons REST et définirons la classe de répartition de votre application de la manière habituelle, sauf que cette fois-ci, vous devrez cocher la case "Utiliser l'authentification JWT" (Use JWT Authentication) ci-dessous. Vous pouvez également choisir d'ajuster les délais d'accès et d'actualisation des jetons, mais les valeurs par défaut sont généralement suffisantes. Vous devez ensuite enregistrer votre application. Le résultat devrait ressembler à la capture d'écran ci-dessous.

Une fois cette opération effectuée, IRIS ajoutera automatiquement quelques points de terminaison à notre API. Par défaut, il s'agira de /login, /logout, /refresh et /revoke. Si vous souhaitez les personnaliser, vous pouvez définir certains paramètres dans votre classe de répartition à cet effet. Par exemple, si je souhaite les faire tous précéder de "/auth", vous pouvez inclure ce qui suit dans votre classe de répartition:
 

Parameter TokenLoginEndpoint = "jwtlogin";
Parameter TokenLogoutEndpoint = "jwtlogout";
Parameter TokenRevokeEndpoint = "jwtrevoke";
Parameter TokenRefreshEndpoint = "jwtrefresh";

Cette opération modifierait tous ces points de terminaison en ajoutant "jwt" devant leur nom. Cependant, pour nos besoins actuels, je préfère conserver leurs valeurs par défaut.

Connexion

Nous allons maintenant nous assurer que ces machines à jetons fonctionnent correctement. Vous pouvez accéder à ces points de terminaison comme vous le feriez pour n'importe quel autre point de terminaison API. Je vais vous donner un exemple d'accès au point de terminaison de connexion à l'aide d'ObjectScript et d'un objet %Net.HttpRequest. Vous pouvez le personnaliser à vos besoins si vous accédez à une telle API depuis IRIS:
 

Class User.JWTTest Extends %RegisteredObject
{
    ClassMethod getToken(Output tokenobj) As %Status
    {
        try{
            set myreq = ##class(%Net.HttpRequest).%New()
            set myobj = ##class(%Library.DynamicObject).%New()
            set myreq.Server = "localhost"
            set myreq.Location = "/iris/jwtauth/login"
            set myreq.ContentType = "application/json"
            do myobj.%Set("user","APIUser")
            do myobj.%Set("password","mypassword")
            do myobj.%ToJSON(myreq.EntityBody)
            do myreq.Post()
            set tokenobj = ##class(%Library.DynamicObject).%FromJSON(myreq.HttpResponse.Data)
            return $$$OK
        }
        catch ex{
            write ex.DisplayString()
            return ex.AsStatus()
        }
    }
}

Voici une astuce rapide pour le dépannage : si vous essayez d'accéder à votre point de terminaison de connexion mais que vous obtenez systématiquement un statut HTTP "404 Page non trouvée", cela est probablement dû au fait que votre application Web n'est pas configurée correctement ou que l'URL à laquelle vous essayez d'accéder est incorrecte. Cependant, vous pouvez également rencontrer une erreur 404 si vous avez oublié de définir le ContentType de la requête sur application/json ou si vous avez essayé d'utiliser une requête GET au lieu d'une requête POST.

À cette étape, nous pouvons exécuter la commande suivante dans une session de terminal dans l'espace de noms ci-dessous:

set sc = ##class(User.JWTTest).getToken(.tokenobj)

Cela nous donnera un objet dynamique contenant la réponse à la demande de connexion. En examinant cet objet, vous observerez plusieurs propriétés:

access_token

Contient la chaîne du jeton d'accès

refresh_token

Contient la chaîne de jeton de rafraîchissement

sub

C'est l'abonné qui indique à qui le jeton est destiné (dans ce cas, il s'agit du nom d'utilisateur que nous avons utilisé pour nous connecter).

iat

Il s'agit de l'horodatage Unix du moment où le jeton a été émis.

exp

Il s'agit de l'horodatage Unix de la période d'expiration du jeton.

Nous pouvons donc utiliser tokenobj.%Get("access_token") pour obtenir le jeton. Mais maintenant que nous avons récupéré un JWT, qu'est-ce que c'est exactement?!

Structure du jeton JWT

Contrairement aux fausses pièces produites en masse de ma jeunesse, chaque jeton JSON Web est unique et distinct. Le jeton d'accès est encodé, comme vous l'avez peut-être deviné en voyant son apparence incompréhensible. Il est également divisé en trois parties séparées par des points. Ces segments comprennent un en-tête, une charge utile et une signature. Les deux premiers sont assez faciles à décoder. Nous pouvons examiner de plus près notre session de terminal à l'aide de quelques commandes ci-dessous:
 

Set token = tokenobj.%Get(“acess_token”)
Set tokenheader = $P(token,”.”,1)
Write $SYSTEM.Encryption.Base64Decode(tokenheader)
Set tokenpayload = $P(token,”.”,2)
Write $SYSTEM.Encryption.Base64Decode(tokenpayload)

Une fois cette opération effectuée, il apparaîtra clairement que ces deux éléments sont simplement des objets JSON encodés en base64. L'en-tête ne comporte que deux champs: le champ alg, qui contient l'algorithme de signature, et le champ typ, qui inclut le type de jeton. Dans ce cas, nous travaillons uniquement avec des jetons JWT, ce qui est reflété ici. Le jeton lui-même conserve le même horodatage d'émission, le même horodatage d'expiration et les mêmes informations sur l'abonné que ceux que nous avons vus dans la réponse à notre demande de jeton initiale. Il intègre également l'émetteur, qui doit correspondre au champ issuer que nous avons saisi dans le portail de gestion du système (l'application web à laquelle nous nous sommes connectés), ainsi qu'un identifiant de session.

La troisième partie du jeton est quelque peu différente des deux autres. Il s'agit d'une signature cryptée selon le paramètre JWT Signature Algorithm (Algorithme de signature JWT) dans le portail de gestion du système. Elle est créée en hachant et en cryptant le contenu des deux sections précédentes. Lorsque le serveur reçoit un jeton, il peut recréer la signature à partir de l'en-tête et de la charge utile. Si la signature reconstruite ne correspond pas à celle du jeton, le serveur conclut que le jeton a été altéré et le rejette.

Utilisation du jeton

Maintenant que nous avons un jeton, nous pouvons jouer! Simplifions au maximum la classe de répartition:
 

Class User.JWTAuth Extends %CSP.REST
{
    XData UrlMap [ XMLNamespace = "http://www.intersystems.com" ]
    {
        <Routes>
            <Route Url="/test" Method="GET" Call="Test" />
        </Routes>
    }
    ClassMethod Test() As %Status
    {
        write "Success!"
        return $$$OK
    }
}

Pour accéder à ce point de terminaison, nous devons d'abord envoyer une requête avec un en-tête d'autorisation contenant "Bearer:" et ensuite le jeton d'accès. Nous allons examiner la méthode suivante, que j'ai placée dans une classe appelée User.JWTTest:
 

ClassMethod getTest(Output myreq) As %Status
{
    try{
        set sc = ##class(User.JWTTest).getToken(.tokenobj)
        set myreq = ##class(%Net.HttpRequest).%New()
        set myobj = ##class(%Library.DynamicObject).%New()
        set myreq.Server = "localhost"
        set myreq.Location = "/iris/jwtauth/test"
        set myreq.Authorization = "Bearer: "_tokenobj.%Get("access_token")
        do myreq.Get()
        return $$$OK
    }
    catch ex{
        return ex.AsStatus()
    }
}

Nous allons maintenant appeler cette méthode dans une session de terminal:

set sc = ##class(User.JWTTest).getTest(.myreq)

Nous pouvons ensuite examiner l'objet de la requête et sa réponse. Lorsque nous examinons la réponse, nous remarquons que le statut HTTP est 200 OK, et si nous affichons les données de la réponse, nous verrons "Success!" comme prévu.

Actualisation

Finalement, nous tomberons inévitablement sur l'écran "insérer des jetons pour continuer" (insert tokens to continue). À ce stade, nous devrons traiter le point de terminaison suivant, /refresh. Si vous avez prêté attention aux paramètres de délai d'expiration, vous aurez remarqué que le jeton d'actualisation a toujours un délai d'expiration plus long que le jeton d'accès. Une fois que le jeton d'accès a expiré, le jeton d'actualisation peut être utilisé pour récupérer un nouveau jeton d'accès et un nouveau jeton d'actualisation, invalidant les anciens jetons dans le processus, sans perdre de session et en en démarrant une nouvelle. Un autre avantage de l'utilisation de ce point de terminaison plutôt que du point de terminaison de connexion est que la connexion apparaîtra dans le journal d'audit du portail de gestion du système si vous l'avez configuré pour enregistrer les événements de connexion. Comme l'actualisation n'est pas une véritable connexion, elle ne sera pas reflétée de cette manière dans le journal d'audit.

Pour utiliser le point de terminaison d'actualisation, nous enverrons une requête très similaire à celle de connexion, à l'exception du corps qui sera un peu différent:
 

{
    "refresh_token": "(your refresh token goes here",
    "grant_type": "refresh_token"
}

Lorsque nous envoyons cette requête, nous obtenons une réponse structurée exactement comme la réponse de la requête de connexion. Elle contient toujours des jetons d'accès et d'actualisation valides. À cette étape, les anciens jetons sont révoqués. Ainsi, même si votre jeton d'actualisation n'a pas encore expiré, vous ne pourrez pas le réutiliser.

Si votre jeton d'actualisation a expiré, cette requête échouera avec une erreur d'autorisation, et dans ce cas, vous devrez vous reconnecter.

Déconnexion

Une fois que nous avons utilisé tous nos jetons et récupéré nos prix, il est temps de rentrer chez nous. Nous avons ici deux options : actualiser et révoquer. Mais pourquoi deux? Lorsque nous avons configuré notre application web, j'ai oublié de mentionner le champ "Group By ID" (Regrouper par ID). Bien que, selon la documentation actuelle, ce champ ne devrait plus être utilisé, vous pouvez encore rencontrer d'anciennes applications où il est défini sur une valeur. Toutes les applications qui partagent un groupe par ID sont également programmées pour partager l'authentification. Cela signifie que lorsque vous vous connectez ou vous déconnectez de l'une d'entre elles, vous êtes connecté ou déconnecté de toutes les autres. Si le groupe par ID est attribué, le point de terminaison de déconnexion déconnectera toutes les sessions qui partagent ce groupe par ID. Si le groupe par ID n'est pas attribué, les deux points de terminaison fonctionnent de la même manière (ils annulent le jeton d'accès actuel et le jeton d'actualisation associé). Ces points de terminaison exigent également une requête POST avec l'en-tête d'autorisation défini. Cependant, ils ne nécessitent pas de corps et ne renvoient pas de réponse avec un corps. Vous saurez qu'ils ont terminé leur travail avec succès car l'état HTTP sera 200 OK.

Après cela, toute tentative d'utilisation du jeton d'accès ou du jeton d'actualisation entraînera un code d'état HTTP 401 non autorisé.

J'espère que cet article vous a été utile et que vous pourrez désormais implémenter ces astuces. Pour ma part, je vais me détendre avec une partie de Donkey Kong!

Discussion (0)2
Connectez-vous ou inscrivez-vous pour continuer