Modelo de apps en detalle (IV): Construyendo una app High-Trust

February 03, 2015

En la última entrega de esta serie de posts sobre el modelo de apps en detalle, hablé de las aplicaciones Low-Trust y aplicaciones High-Trust de SharePoint 2013. Pues bien, hoy vamos a meternos en la harina y hacer una aplicación high-trust (también conocida como S2S, server-to-server) desde cero.

Preparando el entorno

Servicio de perfiles

Para comenzar a crear una aplicación high-trust, necesitamos configurar varias cosas en nuestro entorno local de SharePoint 2013. (Recordáis que las app high-trust sólo se pueden tener en un SharePoint on-premise, ¿verdad?). Las configuraciones no son muchas, pero es fácil olvidarse de una de ellas y luego tendremos problemas para investigar de donde viene el error.

Antes que nada, necesitamos que nuestro SharePoint 2013 tenga activo el servicio de perfiles de usuario y que además tenga indexados los perfiles de los usuarios que vamos a utilizar en la aplicación. Esto es necesario porque el servicio de autenticación de una app high-trust necesita "encontrar" el usuario en el servicio de perfiles de SharePoint para poder ejecutar las consultas en su nombre. Si el perfil del usuario no está, la autenticación fallará.

image

Realmente, el token de acceso que envía la app hacia SharePoint contiene el identificador del usuario, y SharePoint se basa en él para saber si el usuario es válido o no, buscándolo en su base de datos de perfiles. El identificador suele ser el SID del usuario Windows, su UPN o nombre de usuario de Active Directory. Si usamos otros sistemas de autenticación como FBA o Claims, los identificadores serán otros. Es estrictamente necesario que el identificador del usuario esté presente en su perfil y que no haya repeticiones. Si os pica mucho la curiosidad, hay un excelente post de Steve Peschka al respecto.

Certificado SSL

Para poder firmar el token de la app, necesitamos un certificado SSL. Mientras desarrollamos, podemos usar un certificado de desarrollo firmado por nosotros mismos (self-signed certificate). Luego, en producción, usaremos un certificado real.

Además, para que nuestra app se pueda comunicar con SharePoint de manera segura, necesitamos que la comunicación esté encriptada bajo HTTPS. Para ello, necesitaremos otro certificado SSL con la URL de la app. Esto no es necesario en desarrollo, donde podemos relajar la restricción y usar HTTP, pero en producción esto sería una imprudencia seria.

Para crear un certificado autofirmado, iremos a la consola de IIS y bajo el apartado "Server Certificates" y dentro de él la opción "Create Self-Signed Certificate". Le daremos el nombre CertificadoHighTrust.

image image image

Al final, exportaremos el certificado incluyendo la clave privada. Como contraseña le pondremos "password". Al final, tendremos un fichero PFX con el certificado digital que usaremos en nuestra app. Este fichero tiene que estar en una carpeta accessible desde Visual Studio. En nuestro caso, como estamos desarrollando en una máquina de SharePoint, no tenemos que mover el fichero y lo tendremos en la ruta C:\Certificates\CertificadoHighTrust.pfx.

image image

También haremos una exportación del certificado sin la clave privada, para obtener el fichero CertificadoHighTrust.cer. Para ello, tenemos que ir a "Server Certificates" dentro del IIS, abrir el certificado y en la pestaña "Details" ir a la opción "Copy to file" indicando que no queremos la clave privada.

image image image

Ahora vamos a comprobar los permisos necesarios para que SharePoint pueda procesar nuestros certificados. Los requerimientos son dos:

  • El application pool SecurityTokenServiceApplicationPool tiene que tener permisos de lectura sobre la carpeta de los certificados
  • El application pool de la aplicación web en la que instalaremos la app (en nuestro caso, la del puerto 80) tiene que tener permisos de lectura sobre la carpeta de los certificados

En nuestro caso, son las cuentas SPFarm y SQLSvc. Les daremos los permisos correspondientes en la carpeta Certificates.

image image

Ahora tenemos que hacer que SharePoint reconozca nuestro certificado. Abrimos una consola PowerShell de SharePoint y registramos el certificado como de confianza.

$publicCertPath = "C:\\Certificates\\CertificadoHighTrust.cer"
$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($publicCertPath)
New-SPTrustedRootAuthority -Name "CertificadoHighTrust" -Certificate $certificate
image

Configurar trusted issuer

Una vez que SharePoint se fía de nuestro certificado, vamos a proceder a configurar lo que se conoce como un "emisor de confianza". Esto no es más que indicarle a SharePoint que los tokens firmados por un "emisor de confianza" son de fiar. Y, ¿cómo sabe SharePoint qué un emisor es de confianza? Primero, el ID del emisor (un GUID que va dentro del token) tiene que existir en la configuración de SharePoint. Segundo, el token tiene que estar firmado por un certificado del que SharePoint se "fía" porque tiene su parte pública. Como esta parte del certificado la hemos hecho ya, sólo falta decirle a SharePoint el ID de nuestro proveedor de confianza. Puede ser cualquier GUID, y aquí vamos a utilizar el aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee (si usamos letras en el GUID, tienen que ser en minúscula). Bonito y fácil de recordar, ¿verdad?

Para registrar nuestro emisor de confianza, hay que ejecutar el siguiente código en PowerShell, a continuación del script de importación del certificado:

\# CONFIGURAR EL TRUSTED ISSUER
$realm = Get-SPAuthenticationRealm
$specificIssuerId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
$fullIssuerIdentifier = $specificIssuerId + '@' + $realm 
New-SPTrustedSecurityTokenIssuer -Name "CertificadoHighTrust" -Certificate $certificate -RegisteredIssuerName $fullIssuerIdentifier –IsTrustBroker
iisreset

 

Ya podemos proceder a desarrollar la app, pero antes de esto vamos a permitir el uso del certificado autofirmado relajando los permisos de autenticación. (ojo: esto se puede hacer sólo en los entornos de desarrollo, nunca en producción).

\# PERMITIR LLAMADAS CON EL CERTIFICADO AUTOFIRMADO
$serviceConfig = Get-SPSecurityTokenServiceConfig
$serviceConfig.AllowOAuthOverHttp = $true
$serviceConfig.Update()

 

Desarrollando la app

La app necesitará el certificado SSL y conocer la contraseña de su parte privada. Además, la cuenta bajo la que se ejecutará la app (el application pool del IIS) tiene que tener permisos para acceder a la ubicación del certificado.

Abrimos Visual Studio 2013 y creamos una app de SharePoint 2013. Al salir el asistente, le indicamos que queremos una app provider-hosted y que la identidad de la app se establecerá mediante certificado.

image image image

Ahora tendremos una aplicación (en mi ejemplo, creada con Web Forms) que muestra el nombre del sitio actual de SharePoint donde está instalada la app. La solución consiste en dos proyectos: el proyecto de la app de SharePoint y el proyecto web donde está la lógica de la app.

image

El código que hace la llamada a SharePoint es muy sencillo:

protected void Page\_Load(object sender, EventArgs e)
{
    // The following code gets the client context and Title property by using TokenHelper.
    // To access other properties, you may need to request permissions on the host web.

    Uri hostWeb = new Uri(Request.QueryString\["SPHostUrl"\]);

    using (var clientContext = TokenHelper.GetS2SClientContextWithWindowsIdentity(hostWeb, Request.LogonUserIdentity))
    {
        clientContext.Load(clientContext.Web, web => web.Title);
        clientContext.ExecuteQuery();
        Response.Write(clientContext.Web.Title);
    }
}

 

 

Como se puede ver, el contexto de SharePoint se establece usando la clase auxiliar TokenHelper con el método GetS2SClientContextWithWindowsIdentity. Esta llamada obtiene un contexto de high-trust app (S2S, server-to-server) usando la identidad del usuario Windows que está ejecutando la aplicación. Esta es la configuración por defecto, pero se puede modificar para usar la identidad federada por ejemplo.

Ejecutando la aplicación, nos sale el diálogo de otorgar permisos a la aplicación, y al aceptarlo, podemos ver el título del sitio de SharePoint, "Home".

image image

Dentro del TokenHelper

Vamos a ver como nuestra aplicación construye el token. Si miramos el método GetS2SClientContextWithWindowsIdentity, veremos que su cuerpo tiene 4 lineas de código.

public static ClientContext GetS2SClientContextWithWindowsIdentity(
    Uri targetApplicationUri,
    WindowsIdentity identity)
{
    string realm = string.IsNullOrEmpty(Realm) ? GetRealmFromTargetUrl(targetApplicationUri) : Realm;

    JsonWebTokenClaim\[\] claims = identity != null ? GetClaimsWithWindowsIdentity(identity) : null;

    string accessToken = GetS2SAccessTokenWithClaims(targetApplicationUri.Authority, realm, claims);

    return GetClientContextWithAccessToken(targetApplicationUri.ToString(), accessToken);
}

Primero se obtiene el dominio (realm) de la aplicación. Acto seguido se obtiene un JWT (JSON Web Token) que contiene los claims del usuario actual de Windows. Una vez que tenemos el token JWT, lo empaquetamos en un token de acceso con el método GetS2SAccessTokenWithClaims. Al final, el token de acceso lo intercambiamos por un contexto cliente de SharePoint.

La parte interesante es ver como se hace el token. Si miramos el método GetS2SAccessTokenWithClaims, veremos que acaba en un método IssueToken, que está construyendo el token de acceso.

private static string IssueToken(
            string sourceApplication,
            string issuerApplication,
            string sourceRealm,
            string targetApplication,
            string targetRealm,
            string targetApplicationHostName,
            bool trustedForDelegation,
            IEnumerable<JsonWebTokenClaim> claims,
            bool appOnly = false)
        {
            if (null == SigningCredentials)
            {
                throw new InvalidOperationException("SigningCredentials was not initialized");
            }

            #region Actor token

            string issuer = string.IsNullOrEmpty(sourceRealm) ? issuerApplication : string.Format("{0}@{1}", issuerApplication, sourceRealm);
            string nameid = string.IsNullOrEmpty(sourceRealm) ? sourceApplication : string.Format("{0}@{1}", sourceApplication, sourceRealm);
            string audience = string.Format("{0}/{1}@{2}", targetApplication, targetApplicationHostName, targetRealm);

            List<JsonWebTokenClaim> actorClaims = new List<JsonWebTokenClaim>();
            actorClaims.Add(new JsonWebTokenClaim(JsonWebTokenConstants.ReservedClaims.NameIdentifier, nameid));
            if (trustedForDelegation && !appOnly)
            {
                actorClaims.Add(new JsonWebTokenClaim(TrustedForImpersonationClaimType, "true"));
            }

            // Create token
            JsonWebSecurityToken actorToken = new JsonWebSecurityToken(
                issuer: issuer,
                audience: audience,
                validFrom: DateTime.UtcNow,
                validTo: DateTime.UtcNow.AddMinutes(TokenLifetimeMinutes),
                signingCredentials: SigningCredentials,
                claims: actorClaims);

            string actorTokenString = new JsonWebSecurityTokenHandler().WriteTokenAsString(actorToken);

            if (appOnly)
            {
                // App-only token is the same as actor token for delegated case
                return actorTokenString;
            }

            #endregion Actor token

            #region Outer token

            List<JsonWebTokenClaim> outerClaims = null == claims ? new List<JsonWebTokenClaim>() : new List<JsonWebTokenClaim>(claims);
            outerClaims.Add(new JsonWebTokenClaim(ActorTokenClaimType, actorTokenString));

            JsonWebSecurityToken jsonToken = new JsonWebSecurityToken(
                nameid, // outer token issuer should match actor token nameid
                audience,
                DateTime.UtcNow,
                DateTime.UtcNow.AddMinutes(10),
                outerClaims);

            string accessToken = new JsonWebSecurityTokenHandler().WriteTokenAsString(jsonToken);

            #endregion Outer token

            return accessToken;
        }

 

El siguiente artículo de MSDN explica las partes del token de acceso que construye nuestra aplicación. En esencia, se construye un token de "actor" que identifica el usuario actual. Este token de actor se hace con los claims del usuario creados previamente. El token se expide para la aplicación actual (parámetro "aud") y lo firma nuestro certificado (propiedad SigningCredentials). Este token interno se rodea de un token externo que no está firmado digitalmente.

Como se puede ver, el misterioso "token de acceso" es nada más que una cadena de texto con datos en formato JSON que describen una identidad de aplicación y de usuario.

La última parte "misteriosa" es saber como obtiene SharePoint el objeto ClientContext a partir de un token? Es muy sencillo: adjunta el token de acceso en la cabecera de la petición a la API, y al volver ya estará inicializado correctamente el contexto. Ahora lo veremos con Fiddler.

Las pruebas con Fiddler

Si abrimos Fiddler para ver el tráfico HTTP entre la aplicación y SharePoint, veremos que la aplicación hace una llamada a la API CSOM (/_vti_bin/client.svc/ProcessQuery). Si miramos la petición, en las cabeceras veremos un parámetro llamado Authentication con el valor "Bearer: " seguido de un texto codificado en Base64. Este es nuestro token de acceso.

image

Si usamos alguna herramienta como JWT.io para descodificar el token, podemos ver su estructura.

image

Para más información sobre la estructura del token, hay un magnífico post de Kirk Evans al respecto.

Conclusión

Espero haber desmitificado un poco el mundo de las aplicaciones High-Trust con este post. Como veréis, nos permite usar el modelo de apps sin tener que estar en la nube, lo que es un paso importante para poder adaptar nuestros desarrollos a los escenarios híbridos que parece que serán mucho más habituales en el futuro.

¿Habéis trabajado con este modelo? ¿Podéis compartir vuestras experiencias? ¡Los comentarios os esperan aquí mismo, debajo de este post!


Profile picture

Written by Edin Kapić Insatiably curious code-writing tinkerer. Geek father. Aviation enthusiast. Cuisine journeyman. Follow me on Twitter