WebAPI Service mit SharePoint User Authentifizierung

Ziel ist es ein WebAPI Service zu erstellen welches Daten von SharePoint zurückliefern soll. Dieses Service wird von einer App konsumiert, daher ist eine OAuth Authentifizierung notwendig und der User soll seine von der SharePoint Umgebung gewohnten Credentials (Benutzername/Passwort) für die Authentifizierung nutzen. Aber der SharePoint-Server selbst darf nicht von außerhalb der Firmennetzes angesprochen werden.

Projektsetup

Wir starten zunächst mit einem simplen Web-Projekt wobei wir WebAPI auswählen und darauf achten keine Authentifizierung auszuwählen. Diese werden wir gleich manuell einfügen.

image

Im nächsten Schritt müssen nun einige Nuget Packages installiert. Über die “Package Manager Console” ist das schnell erledigt. Wir benötigen zunächst OWin. Die Befehle dafür:

Install-Package Microsoft.AspNet.WebApi.Owin
Install-Package Microsoft.Owin.Host.SystemWeb

Weiters benötigen wir von OWin das Identity System und da wir OAuth verwenden wollen auch das OAuth Modul:

install-package Microsoft.AspNet.Identity.Owin
Install-package microsoft.Owin.Security

Install-Package Microsoft.Owin.Security.OAuth

Das Webservice soll später von JavaScript Code aufgerufen werden, der nicht in der selben Domain wie das WebAPI Service liegt. Unser WebService soll also CORS (Cross-origin resource sharing) unterstützen. Zum Glück gibt es auch hierfür bereits eine fertige OWin Implementierung. Also fügen wir noch ein NuGetPackage hinzu:

Install-Package Microsoft.Owin.Cors

Implementierung

Nun geht es ans Coden. Wir müssen zunächst eine OWin Startup Klasse hinzufügen. Die Startup Klasse nenne ich Startup. Im Visual Studio gibt es eine Vorlage für Startup Klasse. Im Bereich Web finden wir das “OWIN Startup class”-Template.

image

Die Klasse selbst ist einfach. Wichtig ist das OWinStartup Attribut welches für das Assembly gesetzt wird. Die Methode die wir implementieren müssen ist Configuration(IAppBuilder app)

OAuth Konfiguration

Um es etwas übersichtlicher zu gestalten habe ich die Konfiguration für OAuth in die Methode ConfigureOAuth(IAppBuilder app) ausgelagert.

1 private void ConfigureOAuth(IAppBuilder app) 2 { 3 OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions() 4 { 5 AllowInsecureHttp = true, 6 TokenEndpointPath = new PathString("/token"), 7 AccessTokenExpireTimeSpan = TimeSpan.FromDays(1), 8 Provider = new SharePointAuthorizationServerProvider() 9 }; 10 11 // Token Generation 12 app.UseOAuthAuthorizationServer(OAuthServerOptions); 13 app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); 14 15 }
In Zeile 5 wird definiert, dass OAuth auch über http zulässig ist. Im Produktivbetrieb muss diese Zeile raus oder mit false geschrieben werden. Wir wollen in der “freien Wildbahn” OAuth nur über https zulassen.

Zeile 6 definiert den Pfad wie der Client zu einem Accesstoken kommt. Und in Zeile 7 wird die Gültigkeitsdauer des Tokens gesetzt.

In Zeile 8 definieren wir welcher AuthorizationProvider verwendet werden soll. Diese Klasse gibt es noch nicht. Wir werden diese aber bald implementieren.

Die eigentliche Configure Methode sieht so aus:

1 public void Configuration(IAppBuilder app) 2 { 3 ConfigureOAuth(app); 4 5 HttpConfiguration config = new HttpConfiguration(); 6 WebApiConfig.Register(config); 7 app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll); 8 app.UseWebApi(config); 9 }

Zunächst rufen wir die OAuth Konfigurationsmethode (von oben) auf und setzen dann die HttpConfiguration. Da wir CORS verwenden wollen, kommt der Aufruf in Zeile 7 dazu.

AuthorizationProvider-Klasse

Im letzten Schritt müssen wir die Klasse “SharePointAuthorizationServerProvider” implementieren. Diese Klasse erbt von OAuthAuthorizationServerProvider und wir überschreiben zwei Methoden. ValidateClientAuthentication und GrantRessourceOwnerCredentials

Die erste Methode bedarf keiner Erklärung.

1 public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) 2 { 3 context.Validated(); 4 }

In der zweiten Methode überprüfen wir Benutzername und Passwort. Diese ist schon etwas länger:

1 public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) 2 { 3 context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" }); 4 // SharePoint... 5 using (ClientContext ctx = new ClientContext("http://SP/")) 6 { 7 ctx.Credentials = new NetworkCredential(context.UserName, context.Password); 8 var u = ctx.Web.CurrentUser; 9 ctx.Load(u, _ => _.Title); 10 try 11 { 12 ctx.ExecuteQuery(); 13 var identity = new ClaimsIdentity(context.Options.AuthenticationType); 14 identity.AddClaim(new Claim("user", u.Title)); 15 identity.AddClaim(new Claim("login", EncryptionHelper.Encrypt(context.UserName))); 16 identity.AddClaim(new Claim("pw", EncryptionHelper.Encrypt(context.Password))); 17 context.Validated(identity); 18 } 19 catch 20 { 21 string error = "u:{0} p:{1}"; 22 context.SetError("invalid_grant", string.Format(error, "Invalid UserName or Password")); 23 } 24 } 25 }

In Zeile 3 fügen wir in den Response einen Header ein, um Aufrufe von anderen Domains zuzulassen.

ab Zeile 5 verwenden wir das CSOM für SharePoint um einen Zugriff auf SharePoint durchzuführen. in Zeile 7 werden neue Networkcredentials gebildet. Dafür wird aus dem Context UserName und Passwort verwendet. Diese beiden Werte müssen bei Abruf des Tokens mitgegeben werden.

In weiteren Aufrufen wird uns das AccessToken übergeben. Möchten wir später wieder auf SharePoint zugreifen müssen wir wieder den ClientContext mit NetworkCredentials bilden und benötigen daher später auch die Informationen UserName und Passwort. Da ich nichts serverseitig speichern möchte und auch Cookies ausfallen, legen ich beide Infos im AccessToken als Claims ab. (Zeile 15 und 16). Da der AccessToken aber nicht verschlüsselt ist, übernimmt das eine Hilfsklasse (EncryptionHelper) für mich und die Credentials für SharePoint werden verschlüsselt als Claims abgelegt um später wieder zur Verfügung zu stehen.

Claims auslesen

Um in einem ValueController wieder auf SharePoint zugreifen zu können müssen wieder UserName und Passwort aus den Claims ermittelt werden. Dafür habe ich mir eine Hilfsfunktion geschrieben, die einen ClientContext für SharePoint zurückliefert:

1 public class SharePointContextHelper 2 { 3 public static ClientContext GetClientContextForCurrentPrincipal() 4 { 5 var identity = (ClaimsPrincipal)Thread.CurrentPrincipal; 6 var claims = identity.Claims; 7 var login = EncryptionHelper.Decrypt(claims.FirstOrDefault(c => c.Type == "login").Value); 8 var pw = EncryptionHelper.Decrypt(claims.FirstOrDefault(c => c.Type == "pw").Value); 9 10 ClientContext ctx = new ClientContext("http://sp/"); 11 ctx.Credentials = new NetworkCredential(login, pw); 12 return ctx; 13 14 } 15 }

Der CurrentPrincipal des Threads ist ein ClaimsPrincipial und diesen können wir nutzen um wieder die Claims des angemeldeten Benutzers zu lesen. In der Funktion werden die Claims für Passwort (pw) und Username (login) ausgelesen und mit Hilfe des EncryptionHelper wieder entschlüsselt.

Testen

Ein Accesstoken kann mittels POST Request auf /Token (Haben wir in der Konfiguration so festgelegt) abgerufen werden. Es müssen als x-www-form-urlencoded Parameter die Werte für username, password und grant_type übergeben werden. Ich verwende zum Testen das Tool “Postman” da damit leicht alle Arten von Requests erstellt werden können.

image

Wenn dieser Request ausgeführt wird, erhalten wir im Response den AccessToken. Diesen verwenden wir gleich im nächsten Request, daher empfiehlt es sich diesen in die Zwischenablage zu kopieren.

image

Um Daten vom WebAPI abzurufen muss nun der entsprechende Controller aufgerufen werden. Beim Aufruf ist im Authorization-Header mitzugeben. In diesem muss “Bearer” gefolgt von einem Leerzeichen und dann das Access-Token übergeben werden.

image

Und nun können wir auf SharePoint Dateizugreifen, ohne SharePoint nach Außen zu veröffentlichen und die User melden sich dennoch mit ihren gewohnten Credentials an.

Kommentare sind geschlossen