Authentification avec AzureAD

Tags: #<Tag:0x00007f9e4e0e8c80>

Bonjour,

J’interface Simplicité à un annuaire AzureAD (compatible OpenID Connect).
Je souhaite donc créer les utilisateurs, les groupes et les relations utilisateurs / groupe au moment de l’authentification.
J’utilise pour cela les GrantHook :
https://www.simplicite.io/resources/documentation/01-core/grant-code-hooks.md
https://www.simplicite.io/resources/4.0/javadoc/com/simplicite/util/GrantHooksInterface.html

Avant de créer l’utilisateur, j’ai également besoin de récupérer le jeton ‘access token’ OpenID Connect pour appeler l’API Microsoft https://graph.microsoft.com et requêter les attributs utilisateurs. Le principe est décrit ici:
https://docs.microsoft.com/fr-fr/azure/active-directory/develop/v2-oauth2-auth-code-flow

Pouvez-vous m’indiquer comment récupérer le jeton ‘access token’ via l’API Java de Simplicité ?
Auriez vous un exemple de code en Java pour créer un utilisateur lors de l’authentification, et attribuer l’utilisateur à des rôles préalablement déclarés ?

Merci pour votre aide.

Voici déjà un exemple de création de user dans le preLoadGrant:

	public static void preLoadGrant(Grant g) {
		Grant sys = Grant.getSystemAdmin();

		if (g.isOAuth2AuthMethod()) try { // only if OAuth2 auth method
			String login = g.getLogin();
			if (Tool.isEmpty(login))
				throw new AuthenticationException("Empty login");

			// Create user if needed
			if (!Grant.exists(login, false)) {
				ObjectDB usr = sys.getTmpObject("User");
				synchronized (usr) { // thread-safe
					usr.resetValues(true);
					usr.setRowId(ObjectField.DEFAULT_ROW_ID);
					usr.getField("usr_login").setValue(login);
					usr.setFieldValue("usr_lang", Globals.LANG_FRENCH);
					usr.setFieldValue("usr_home_id", View.getViewId(sys.getParameter("DEFAULT_USER_HOME", "Home")));
					usr.setStatus(Grant.USER_ACTIVE);
					AppLog.info(GrantHooks.class, "preLoadGrant", "User to create " + usr.toJSONObject().toString(2), sys);
					new BusinessObjectTool(usr).validateAndCreate();

					String module = usr.getFieldValue("row_module_id.mdl_name");
					AppLog.info(GrantHooks.class, "preLoadGrant", "Created user " + login + " in module " + module, sys);

					String group = sys.getParameter("DEFAULT_USER_GROUP", "");
					if (!Tool.isEmpty(group)) {
						Grant.addResponsibility(usr.getRowId(), group, Tool.getCurrentDate(-1), "", true, module);
						AppLog.info(GrantHooks.class, "preLoadGrant", "Added " + group + " responsibility for user " + login, sys);
					}
				}
			}
		} catch (Exception e) {
			AppLog.error(GrantHooks.class, "preLoadGrant", null, e, sys);
		}
	}

Je regarde le point sur le changement du login et je te fais un retour.

Pour les appels de services je te recommande d’utiliser RESTTool cf. https://docs.simplicite.io/4.0/javadoc/com/simplicite/util/tools/RESTTool.html

Pour préciser le besoin :
Dans le protocole OpenID Connect, lors de l’appel au endpoint /token, on reçoit une réponse de ce type :

{
“access_token”: “eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1Q…”,
“token_type”: “Bearer”,
“expires_in”: 3599,
“scope”: “https%3A%2F%2Fgraph.microsoft.com%2Fmail.read”,
“refresh_token”: “AwABAAAAvPM1KaPlrEqdFSBzjqfTGAMxZGUTdM0t4B4…”,
“id_token”: “eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJhdWQiOiIyZDRkMTFhMi1mODE0LTQ2YTctOD…”,
}

Simplicité décode le ‘id_token’, et on retrouve cet ‘id_token’ dans l’objet SessionInfo.
Mais par contre, l’API ne fournit pas d’accès aux tokens “access_token” et “refresh_token” qui sont nécessaires pour appeler des API gérées par le fournisseur d’identité OIDC.

La classe GoogleAPITool semble fournir ces infos via les méthodes getToken & refreshToken ?
Il faudrait idéalement avoir ce type de méthode pour tout type de provider OIDC.

Je vais vérifier mais les infos obtenues lors des échanges d’autent sont normalement toutes conservées dans le session info. Je vous tiens au courant

Effectivement, contrairement au cas particulier OAuth2 Google, toutes les infos ne sont pas conservées dans le cas du connecteur générique OpenIDConnect. On est en train de faire les évolutions pour que ce soit le cas.

Je vous tiendrai au courant quand ce sera backporté sur les branches (preàrelease.

C’est en place (testé avec Google en mode OpenIDConnect) j’espère que ça marchera iso dans votre cas.

Les 3 tokens sont désormais disponibles dans le session info SessionInfo info = <grant>.getSessionInfo() (où <grant> dépend du contexte, ex: dans un objet interne ou externe c’est getGrant()). Cf. https://docs.simplicite.io/4.0/javadoc/com/simplicite/util/SessionInfo.html

  • Access token: info.getToken()
  • ID token: info.getIDToken()
  • Refresh token: info.getRefreshToken() (avec Google celui ci n’est pas systématiquement fourni)

ex: traces dans un GratHooks.preLoadGrant():

public static void preLoadGrant(Grant g) {
	try {
		AppLog.info(GrantHooks.class, "postLoadGrant",
			"Grant " + g.getLogin() +
			", access token " + g.getSessionInfo().getToken() +
			", ID token " + g.getSessionInfo().getIDToken() +
			", refresh token " + g.getSessionInfo().getRefreshToken()
			", session info " + g.getSessionInfo().toString(), null);
	} catch (Throwable e) {
		AppLog.error(GrantHooks.class, "preLoadGrant", null, e, null);
	}
}

Attention dans un preLoadGrant le grant g ne contient pas encore grand chose, essentiellement le login et, justement, le session info issu de l’authentification, le reste se charge plus tard dans le load.

Bonjour,

Notre plateforme a été mise à jour avec la dernière version (4.0 patch level P24Built on2020-03-04 22:57).
J’ai maintenant accès aux 3 tokens OIDC (access, refresh et ID token) et je peux récupérer les attributs utilisateurs en appelant l’API Microsoft Graph.

Par contre, le premier code d’exemple ci-dessus ne fonctionne pas comme voulu.

Avec AzureAD, l’appel au enpoint /userinfo renvoi une chaîne très limitée du type :
{
“sub”: “Q5IqbVBjeZIUtkBs9yLm-Mg3nhobR_N2y1XC7Sn6HLU”,
“name”: “Alice Green”,
“family_name”: “Green”,
“given_name”: “Alice”,
“picture”: “https://graph.microsoft.com/v1.0/me/photo/$value
}

Dans le preLoadGrant, je crée donc un utilisateur s’il n’existe pas, avec les attributs récupérés auparavant avec l’API Microsoft.
Je modifie le login pour remplacer l’ID opaque (champ ‘sub’) fourni par AzureAD par le vrai login (champ ‘upn’ ou userPrincipalName, accessible dans l’access token ou le ID token).
=> dans preLoadGrant(Grant g), j’appelle donc g.setLogin(login)
Le résultat est que je me retrouve avec 2 users en base :

  • celui que j’ai créé
  • celui avec le login opaque
    De plus je suis loggé ave le premier user et donc je n’ai pas les bons droits.

J’ai tenté d’intervenir en amont via la variable OAUTH2_USERINFO_MAPPINGS, l’idée étant de remplacer au plus tôt le login
{ “login”: “upn” }
Mais le mapping ne semble lire que les champs récupérés auprès de OAUTH2_USERINFO_URL donc il ne se passe rien.

J’ai tenté d’intervenir auprès le la méthode GrantHooks.parseAuth mais cette méthode n’est pas appelée.

Existe-t-il un moyen de créer les utilisateurs à la volée en leur attribuant un login spécifique ?

En l’état, le mapping “login” ne cherche effectivement que dans le JSON userinfo.

SI je comprend bien, l’attribut upn n’est pas dans le userinfo mais dans la réponse du token. Puis-je avoir un exemple ? Merci

Voici un exemple d’access token décodé et de ID token décodé.
Ici, j’ai configuré Azure AD pour qu’il renvoi le champ upn (entre autre).
Le principe est décrit ici: https://docs.microsoft.com/fr-fr/azure/active-directory/develop/active-directory-optional-claims#configuring-groups-optional-claims
Mais je peux aussi récupérer le login ou tout autre attribut via un appel à l’API Microsoft dans la méthode preLoadGrant.

Access token

{
“typ”: “JWT”,
“nonce”: “Mq85GOUCbOdUbLPj6Dg0uVEoXwfQqdul59tb3yE5dAc”,
“alg”: “RS256”,
“x5t”: “HlC0R12skxNZ1WQwmjOF_6t_tDE”,
“kid”: “HlC0R12skxNZ1WQwmjOF_6t_tDE”
}.{
“aud”: “00000003-0000-0000-c000-000000000000”,
“iss”: “https://sts.windows.net/3ec31dbb-6f03-4141-a210-63354b279757/”,
“iat”: 1581590321,
“nbf”: 1581590321,
“exp”: 1581594221,
“acct”: 0,
“acr”: “1”,
“aio”: “42NgYLALMYxb11bR9ebYTMGEyf6d12e7nl9pW2W/O6vpi92Fo98B”,
“amr”: [
“pwd”
],
“app_displayname”: “NAMe”,
“appid”: “1abed652-a82d-4f81-b813-5018232fcaa8”,
“appidacr”: “1”,
“family_name”: “Green”,
“given_name”: “Alice”,
“ipaddr”: “194.2.202.85”,
“name”: “Alice Green”,
“oid”: “96bc219c-5361-43d0-96e9-f448259d4035”,
“platf”: “3”,
“puid”: “1003200099B3E5A1”,
“scp”: “Directory.Read.All User.Read User.Read.All profile openid email”,
“signin_state”: [
“kmsi”
],
“sub”: “HPuGsG2poodsPyTThMOHptrmIt3XGsKpoeUsNlOZgS0”,
“tid”: “3ec31dbb-6f03-4141-a210-63354b279757”,
“unique_name”: “alice@epidead.onmicrosoft.com”,
“upn”: “alice@epidead.onmicrosoft.com”,
“uti”: “zFdBL-nRA0eGt44lvqydAA”,
“ver”: “1.0”,
“xms_st”: {
“sub”: “Q5IqbVBjeZIUtkBs9yLm-Mg3nhobR_N2y1XC7Sn6HLU”
},
“xms_tcdt”: 1580897789
}.[Signature]

ID Token

{
“typ”: “JWT”,
“alg”: “RS256”,
“kid”: “HlC0R12skxNZ1WQwmjOF_6t_tDE”
}.{
“aud”: “1abed652-a82d-4f81-b813-5018232fcaa8”,
“iss”: “https://login.microsoftonline.com/3ec31dbb-6f03-4141-a210-63354b279757/v2.0”,
“iat”: 1581591215,
“nbf”: 1581591215,
“exp”: 1581595115,
“acct”: 0,
“aio”: “ATQAy/8OAAAAGBGEpBylxNQpgormBkzkgS5rubTguO1oOe95y5r7if/YsaZyyFVpka9N/j86ErB/”,
“family_name”: “Green”,
“given_name”: “Alice”,
“groups”: [
“1eab7446-2bec-4600-b359-ca6d5a1261af”
],
“ipaddr”: “194.2.202.85”,
“name”: “Alice Green”,
“oid”: “96bc219c-5361-43d0-96e9-f448259d4035”,
“preferred_username”: “alice@epidead.onmicrosoft.com”,
“roles”: [
“Writer”
],
“sub”: “Q5IqbVBjeZIUtkBs9yLm-Mg3nhobR_N2y1XC7Sn6HLU”,
“tid”: “3ec31dbb-6f03-4141-a210-63354b279757”,
“upn”: “alice@epidead.onmicrosoft.com”,
“uti”: “xa3Dy54x8kCoQg0ySoSSAA”,
“ver”: “2.0”
}.[Signature]

Nous avons travaillé sur la version de DEV (la future version 5.0) sur une évolution des GrantHooks qui devrait permettre de mettre en place une logique comme celle dont vous avez besoin ainsi que d’autres besoins du même genre.

C’est une évolution structurante qui impacte tous les modes d’authentification supportés par Simplicité.

Donc avant de le backporter sur la release actuelle (4.0.P24), nous devons faire des tests de non régression de tous les modes d’authentification y compris sur nombre de cas particuliers dont nous avons connaissance.

Nous allons donc procéder en passant par une phase de prerelease où nous ne pousserons le backport que sur les branches “prerelease” et “prerelease-light” (= images Docker “beta” et “beta-light”).

Ce sera fait demain dans la journée (si les tests préalables sont concluants). Je vous tiendrai au courant et vous fournirai des exemples.

Ensuite, en fonction des retours des tests de nos clients et partenaires - y compris vous - sur la prerelease nous pousserons ce backport sur les branches “release” et “release-light” (= images Docker “latest” et “latest-light”).

L’évolution en question a été poussée sur les branches prerelease et prerelease-light (images Docker beta et beta-light).

La logique c’est d’implémenter le grant hook parseAuth (le nouveau = celui avec le session info en argument cf. https://docs.simplicite.io/4.0/javadoc-beta/com/simplicite/util/GrantHooksInterface.html#parseAuth(com.simplicite.util.Grant,com.simplicite.util.SessionInfo)

Ce hook renvoie le login qui sera utilisé par Simplicité dans la suite du traitement.

Exemple avec le provider OpenIDConnect de Google (ici on decode le ID token et on récupère le claim “email” pour le login, mais on peut imaginer faire ici d’autres choses comme appeler des service GraphQL etc.)

public static String parseAuth(Grant sys, SessionInfo info) {
	AppLog.info(GrantHooks.class, "parseAuth", "Session info: " + info.toString(), sys);

	String login = info.getLogin(); // Get login as set by the IdP (e.g. sub)
		
	try {
		String idToken = info.getIDToken();
		if (!Tool.isEmpty(idToken)) {
			AppLog.info(GrantHooks.class, "parseAuth", "ID token: " + idToken, sys);
			JSONObject decodedIdToken = AuthTool.decodeJWTToken(idToken);
			AppLog.info(GrantHooks.class, "parseAuth", "Decoded ID token: " + decodedIdToken.toString(2), sys);
			JSONObject claims = decodedIdToken.optJSONObject("claims");
			if (claims!=null) {
				String email = claims.optString("email");
				if (email!=null) {
					AppLog.info(GrantHooks.class, "parseAuth", "Email from ID token: " + email, sys);
					login = email;
				}
			}
		}
	} catch (Throwable e) {
		AppLog.error(GrantHooks.class, "parseAuth", null, e, sys);
	}

	return login;
}

Bonjour,

Le changement de login dans la méthode parseAuth fonctionne.

Quelques difficultés rencontrées et contournées dans la méthode preLoadGrant :

  • l’appel à isOAuth2AuthMethod renvoit toujours true (que l’on soit en authentification locale ou avec OpenID Connect).
  • le jeton access token obtenu avec SessionInfo.getToken() contient également une valeur avec une authentification locale.
    J’arrive cependant à déterminer si on utilise une authentification locale ou non avec un test sur SessionInfo.getIDToken().

Avec l’exemple de création de user au début de ce ticket, le hook est appelé 2 fois
=> la première fois, if (!Grant.exists(login, false)) { … } retourne true
=> la seconde fois, if (!Grant.exists(login, false)) { … } retourne false
Je suppose que c’est voulu.

Avec AzureAD, il faut aussi utiliser “sync”:false dans AUTH_PROVIDERS pour que cela fonctionne.
Sinon, Simplicité renvoit une exception en essayant de lire les infos de OAUTH2_USERINFO_URL
Par exemple AzureAD renvoit ceci :
https://graph.microsoft.com/oidc/userinfo
{
“sub”: “Q5IqbVBjeZIUtkBs9yLm-Mg3nhobR_N2y1XC7Sn6HLU”,
“name”: “Alice Green”,
“family_name”: “Green”,
“given_name”: “Alice”,
“picture”: “https://graph.microsoft.com/v1.0/me/photo/$value
}

et Simplicité n’arrive pas à lire le champ picture :
2020-04-22 19:03:16,833 ERROR [com.simplicite.util.ObjectDirect] SIMPLICITE|http://ef2856201aa5:8080||ECORED0001|system|com.simplicite.util.ObjectDirect|save||Error User
java.lang.NullPointerException
at com.simplicite.util.tools.DocTool.buildThumbImage(DocTool.java:721)
at com.simplicite.util.tools.DocTool.upload(DocTool.java:623)
at com.simplicite.util.engine.ObjectManager.upload(ObjectManager.java:1950)
at com.simplicite.util.engine.ObjectManager.create(ObjectManager.java:1866)
at com.simplicite.util.engine.ObjectManager.save(ObjectManager.java:2906)
at com.simplicite.util.ObjectDirect.save(ObjectDirect.java:441)
at com.simplicite.util.ObjectDB.save(ObjectDB.java:1135)
at com.simplicite.util.ObjectDB.save(ObjectDB.java:1122)
at com.simplicite.util.SessionInfo.syncUser(SessionInfo.java:450)
at com.simplicite.util.Grant.init(Grant.java:341)
at com.simplicite.webapp.tools.ServletTool.getGrant(ServletTool.java:1732)
at com.simplicite.webapp.filters.AuthMethodFilter.getGrant(AuthMethodFilter.java:67)
at com.simplicite.webapp.filters.AuthMethodFilter.doFilter(AuthMethodFilter.java:115)
at com.simplicite.webapp.filters.AbstractFilter.doFilter(AbstractFilter.java:37)

OK super

l’appel à isOAuth2AuthMethod renvoit toujours true (que l’on soit en authentification locale ou avec OpenID Connect).

Oui l’authent interne est aussi un provider de la famille “OAuth2” (au sens large), pour savoir plus de quel provider on parle il faut utiliser SessionInfo.getProvider (cf. https://docs.simplicite.io/4.0/javadoc/com/simplicite/util/SessionInfo.html#getProvider())

Je regarderai le pb sur la photo mais l’URL me semble douteuse c’est quoi le $value ? Faut il le substituer avec quelquechose ? etc.

PS: Il est facile d’inhiber des attributs de la synchro avec le mapping (en mappant sur un nom qui n’existe pas)

Le $value c’est le format microsoft pour appeler leur API.
Exemple ici:
https://developer.microsoft.com/en-us/graph/graph-explorer
=> essayer “GET my photo” dans le menu gauche.
Ce n’est pas gênant qu’on ne puisse pas récupérer les données de OAUTH2_USERINFO_URL mais ça ne devrait pas générer d’exception.

La synchro marche très bien (y compris la photo) avec Google et LiveID.

Pour la photo c’est un simple download de l’URL du user info, forcément que dans votre cas si on downloade “https://graph.microsoft.com/v1.0/me/photo/$value” ça peut pas marcher correctement si ce qu’on récupère comme ça n’est pas directement une image… je vais regarder pour voir si je peux au moins rendre ça plus robuste.

Le mieux en attendant c’est d’inhiber la photo en mettant "picture": "_n_importe_quoi_" dans le mapping user info (param système OAUTH2_USERINFO_MAPPINGS ou directement dans le JSON du param systèmeAUTH_PROVIDERSpour votre provider sur la cléuserinfo_mappings`), comme ça les autres attributs (nom, prénom, email, et téléphone) seront eux synchronisés