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.
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.
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.
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.
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.
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.