Accéder à une API sécurisée par WSO2 API Manager

WSO2, éditeur OSS encore peu connu et peu implanté en France, propose toute une panoplie de solutions middle-end de qualité. Petit focus sur l’une de ces briques : le gestionnaire d’API.

Dans son précédent billet, David décrit l’apport d’une passerelle (“API Gateway”) dans la gestion des API web. Il mentionne en particulier les possibilités offertes par une brique de ce type pour sécuriser l’accès aux API.

A travers cet article, je vous propose de découvrir comment sécuriser une API avec la très complète solution OSS WSO2 API Manager et de quelle façon la consommer.

Une brève description de la solution

Les briques fonctionnelles

La solution de gestion des API web proposée par WSO2 dispose de quatre entités :

  1. une interface de création et de publication des API (Publisher),
  2. une interface proposant un catalogue des API disponibles et la possibilité d’y souscrire pour pouvoir les consommer (Store),
  3. une passerelle (API Gateway) dans laquelle les API sont publiées pour être exposées et permettant de gérer leurs versions et leurs accès (autorisations, traçage, politique de throttling, etc.),
  4. une interface d’administration permettant entre autre de gérer les utilisateurs des différentes interfaces (comptes, rôles, droits, etc.).

L’architecture de la solution

WSO2 API Manager se base sur trois grandes briques de l’éditeur : son ESB, son registre de services et son serveur d’identités.

Définir une API et en sécuriser l’accès

L’interface WSO2 API Publisher permet aux utilisateurs autorisés de créer et publier des API. Dans les grandes lignes, cette application permet :

  • de lister l’ensemble des API disponibles et leurs abonnés
  • de rechercher une API
  • d’ajouter une nouvelle API
  • de visualiser un ensemble de statistiques sur l’usage des API
  • de gérer le cycle de vie des API

Concrètement, une API web créée avec le Publisher est un proxy vers une API web dont on souhaite contrôler l’accès. La définition d’un nouveau proxy peut se faire très simplement à travers l’import d’une description Swagger ou bien via un formulaire en trois étapes :

Etape 1 – Design : on définit le nom, le contexte, le numéro de version et les méthodes d’accès disponibles pour le proxy

WSO2APIMngr_Publisher_Design1

WSO2APIMngr_Publisher_Design2

Etape 2 – Implementation : permet de renseigner les informations relatives à l’API à sécuriser, pour laquelle on crée un proxy

WSO2APIMngr_Publisher_Implement

Etape 3 – Manage : on paramètre le proxy, avec par exemple les modes d’accès autorisés, les SLA, le nombre d’accès maximum autorisés pour cette ressource dans une période de temps ou encore le type de jeton d’authentification requis pour invoquer les différents verbes de l’API

WSO2APIMngr_Publisher_Manage1

WSO2APIMngr_Publisher_Manage2

Durant cette dernière étape, il est possible de définir des portées (scopes). Dans chacune de ces portées on peut lister un ensemble de rôles. En assignant une portée à un verbe de l’API, on limite la possibilité de l’exécuter aux utilisateurs ayant l’un des rôles listés.

A l’issue de ces trois étapes, un proxy est ainsi créé et publié sur la passerelle. Voyons à présent comment utiliser ce proxy et consommer l’API qu’il protège.

Accéder à une API sécurisée

Application Access Token vs User Access Token

Zoomons rapidement sur le point qui nous intéresse le plus dans la configuration mise en place dans le Publisher, à savoir les deux types de jeton d’authentification proposés par WSO2 API Manager :

  1. Le jeton d’accès utilisateur (User Access Token) : il identifie un utilisateur et lui permet d’exécuter toutes les API auxquelles il a préalablement souscrit.
  2. Le jeton d’accès applicatif (Application Access Token) : un seul jeton de ce type permet d’invoquer un ensemble d’API regroupées logiquement pour former une Application. Le mécanisme établi autour de ce jeton et de sa gestion se base sur le protocole OAuth2.

Dans ce qui suit, nous allons nous focaliser sur la manière d’accéder à une API à partir d’un jeton d’accès applicatif en commençant par une description du protocole sous-jacent.

OAuth2

Objectif : permettre à une entité tierce (client) d’accéder aux données privées d’une entité (resource owner) stockées sur un serveur donné (resource server) sans que cette entité ait besoin de communiquer son identifiant et son mot de passe, et potentiellement seulement pour une durée déterminée.

Solution : introduire une couche intermédiaire (authorization server) avec laquelle le client doit dialoguer pour obtenir une autorisation d’accès aux données sensibles.

L’autorisation d’accès délivrée peut avoir différentes formes. Nous nous focaliserons dans cet article sur l’authorization grant code : au niveau du serveur d’autorisation, après acceptation par le resource owner de la demande d’accès du client, un code (une chaîne de caractères alphanumériques) est généré et transmis au client.

A partir de ce code, le client demande au serveur d’autorisation un jeton d’accès qui servira d’accréditation pour accéder aux ressources privées en lieu et place du login/mdp du resource owner.

Illustration du processus :

WSO2APIMngr_OAuth2_AuthorizationCodeGrant

S’enregistrer en tant que client d’un proxy

Avant de pouvoir utiliser un proxy déployé sur la passerelle, un client doit y souscrire. Cette souscription s’effectue dans le Store. Après avoir créé son compte et s’être connecté au Store, le client doit commencer par définir ses applications. Pour rappel, une application est entendue dans WSO2 API Manager comme un regroupement d’API auxquelles un client pourra accéder à partir d’un même jeton d’accès applicatif. Pour créer une nouvelle application, il suffit de saisir un nom et une URL de callback. Cette URL de callback doit correspondre à l’URL vers laquelle le client devra être redirigé après l’étape d’autorisation (voir plus bas).

WSO2APIMngr_Store_MyApplications

Une fois l’application définie, le client recherche le proxy auquel il souhaite souscrire et sélectionne l’application à laquelle l’associer.

WSO2APIMngr_Store_Subscription

Consumer Key et Consumer Secret

Pour chaque application définie dans le Store par le client, un couple [client id | client secret] est généré, permettant d’identifier et d’authentifier l’application consommatrice du proxy. Le client id est utilisé dans les étapes d’autorisation et d’obtention d’un jeton d’accès tandis que le client secret est utilisé uniquement pour récupérer un jeton d’accès.

WSO2APIMngr_Publisher_Keys

Appeler le proxy pour consommer l’API protégée

NB : dans les exemples de code qui suivent sont utilisés :

  • Apache Oltu 1.0.0 comme client OAuth2 (requêtes pour l’autorisation, la récupération du jeton d’accès et l’appel à l’API web sécurisée) ;
  • Jackson 2.3.3 pour lire la réponse au format JSON retournée par l’API invoquée.

Autorisation d’accès

Conformément à la description du processus d’authorization grant code de OAuth2, la première étape pour consommer une API est d’en demander l’autorisation à son propriétaire. Pour cela, il suffit que l’application cliente souhaitant consommer l’API transmette une demande d’autorisation. Pour WSO2 API Manager, cette demande est représentée par une redirection de l’application cliente vers l’url d’autorisation (https://[hostname]:[port]/authorize) avec comme paramètres :

  1. l’identifiant de l’application effectuant la demande (la consumer key récupérée du Store) ,
  2. l’url vers laquelle le client sera redirigé après acceptation de la demande d’accès (une page particulière de l’application cliente par exemple ; cette URL doit en tout cas être identique à l’URL de callback spécifiée dans la définition de l’application faite au niveau du Store) ,
  3. une portée (scope) permettant de préciser avec quels rôles on souhaite pouvoir accéder à l’API,
  4. le type d’autorisation attendu, dans notre cas code pour l’authorization grant code.

Un exemple de code pour que le client demande une autorisation d’accès :

private void authorize(HttpServletResponse response) throws IOException, OAuthSystemException {

		OAuthClientRequest request = OAuthClientRequest
				   .authorizationLocation(AUTHORIZATION_LOCATION)
				   .setClientId(CONSUMER_KEY)
				   .setRedirectURI(REDIRECT_URI)
				   .setScope(SCOPE)
				   .setResponseType(AUTHORIZATION_RESPONSE_TYPE)
				   .buildQueryMessage();

		response.sendRedirect(request.getLocationUri());

	}

Une fois connecté au serveur d’identité, l’utilisateur peut approuver la demande :

WSO2APIMngr_IDServer_Login

WSO2APIMngr_IDServer_Authorization

Après acceptation, une redirection est effectuée vers l’url de callback spécifiée avec en paramètre le code d’autorisation généré.

Récupération d’un jeton d’accès applicatif

A partir du code retourné par l’API d’autorisation, le client peut demander un jeton d’accès via une requête à https://[hostname]:[port]/token. En plus des paramètres déjà transmis lors de la demande d’autorisation, le consumer secret est également envoyé.

private String token(String authorizationCode) throws OAuthSystemException, OAuthProblemException{

		OAuthClientRequest request = OAuthClientRequest
                        .tokenLocation(TOKEN_LOCATION)
                        .setGrantType(GrantType.AUTHORIZATION_CODE)
                        .setClientId(CONSUMER_KEY)
                        .setClientSecret(CONSUMER_SECRET)
                        .setRedirectURI(REDIRECT_URI)
                        .setScope(SCOPE)
                        .setCode(authorizationCode)
                        .buildQueryMessage();

		OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
		OAuthJSONAccessTokenResponse oAuthResponse = oAuthClient.accessToken(request);

		return oAuthResponse.getAccessToken();

	}

Invocation de l’API

Ne reste plus qu’à invoquer le proxy à partir du jeton d’accès :

private String consumeAPI(String accessToken) throws IOException, 
                                                             OAuthSystemException, 
                                                             OAuthProblemException, 
                                                             InvalidCredentialsException {

		OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
		OAuthClientRequest bearerClientRequest = new OAuthBearerClientRequest(MENU_API).buildQueryMessage();
		bearerClientRequest.addHeader("Authorization", "Bearer " + accessToken);
		OAuthResourceResponse response = 
			oAuthClient.resource(bearerClientRequest, OAuth.HttpMethod.GET, OAuthResourceResponse.class);

		StringBuilder sb = new StringBuilder("");
		if (response.getResponseCode() == 200) {
			ObjectMapper mapper = new ObjectMapper();
			List<Dish> menu = mapper.readValue(response.getBody(),new TypeReference<List<Dish>>() {});
			for (Dish dish : menu) {
				sb.append("NOM : ");
				sb.append(dish.getName());
				sb.append("\nDESCRIPTION : ");
				sb.append(dish.getDescription());
				sb.append("\nPRIX : ");
				sb.append(dish.getPrice());
				sb.append("\n\n");
			}
		}
		else {
			throw new InvalidCredentialsException(response.getBody());
		}

		return sb.toString();

	}

Orchestration dans le client

Voici un petit exemple pour orchestrer les différentes étapes décrites ci-dessus (clairement à optimiser, mais c’est pour l’exemple), sachant qu’on peut profiter de la durée de vie du jeton d’accès pour le rejouer sans en demander un nouveau à chaque requête.

protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {

		//As the access token has a time to live duration it can be re-used until it expires.

		String accessToken = loadToken();
		PrintWriter pw = response.getWriter();
		try {
			if (accessToken != null && !accessToken.equalsIgnoreCase("")) {
				String responseToClient = consumeAPI(accessToken);
				response.setStatus(HttpServletResponse.SC_OK);
				pw.write(responseToClient);
			} else {
				try{
					OAuthAuthzResponse oar = OAuthAuthzResponse.oauthCodeAuthzResponse(request);
					String code = oar.getCode();
					if (code != null && !code.equalsIgnoreCase("")){
						//We are here just after the authorization step in the OAuth2 process.

						//We can retrieve a valid access token with the authorization code.

						accessToken = token(code);
						//The new access token is stored.

						storeToken(accessToken);
						//The API is consumed with the valid token

						String responseToClient = consumeAPI(accessToken);
						response.setStatus(HttpServletResponse.SC_OK);
						pw.write(responseToClient);
					}
				} catch (OAuthProblemException e) {
					// If none code parameter has been found in the oauth response, then authorization is required.

					authorize((HttpServletResponse) response);
				}
			}
		} catch (Exception e) {
			response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
			pw.write("API inaccessible");
		} 
		pw.flush();
		pw.close();
	}

Conclusion

WSO2 API Manager propose une solution simple pour sécuriser les accès aux API web en introduisant une passerelle contrôlant les accès. La complexité de cette sécurisation réside en fait dans la compréhension du principal protocole sous-jacent, OAuth2.

Ce qui a été illustré dans cet article peut se résumer avec le schéma suivant :

WSO2APIMngr_Architecture_Copyright

Sachez qu’il est possible de définir dans l’API Manager un fournisseur d’identité tierce et que, si celui-ci produit un jeton SAMLv2, le WSO2 API Manager permet de créer automatiquement un jeton d’accès OAuth2 à partir d’une assertion SAMLv2 : l’étape d’autorisation décrite plus haut est alors remplacée par une relation de confiance permettant un accès type SSO à l’API sécurisée.

Peut-être l’objet d’un prochain billet 😉

Références