WCF LiveID Authentication einer Windows 8 Store App – Teil 4

25. Januar 2013

div class=”articleAbstract”>In diesem Teil der Artikelserie wird für den WCF Service eine Authentifizierung durch Windows Live implementiert.

Überblick zur Artikelserie

Teil 1 gibt eine Übersicht über die eingesetzten Technologien und Portale
Teil 2 beschäftigt sich mit der Erzeugung und Konfiguration der Windows 8 Store App mit Live Anmeldung
Teil 3 erzeugt den WCF Service und sichert diesen über SSL ab
Teil 4 autorisiert den angemeldeten Live-Benutzer an den WCF Services
Teil 5 hosted den Service in Windows Azure

Teil 4 – Autorisierung des WCF Services mit LiveID

Ausgangsituation ist die Solution aus Teil 3 der Artikelserie. Diese besteht aus einer Windows Store App Anwendung, die sich bei Windows Live authentifizieren kann. Diese Clientanwendung ruft einen mit SSL transportgesicherten WCF Service auf.
In Teil 4 wird der WCF Service um eine Autorisierung erweitert. Diese prüft, ob der Benutzer sich mit einer gültigen LiveID angemeldet hat. Damit wird sichergestellt, dass die durch Live gelieferte UserID nicht manipuliert ist und der Benutzer somit eindeutig und sicher identifiziert werden kann.

image

Durchgeführt wird die Autorisierung mit dem AuthenticationToken der Live-Anmeldung aus Teil 2. Dieses Token ist ein Json Web Token (JWT), das unter anderem UserID und Claims des Benutzers enthält. Das JWT ist bei der Live-Anmeldung mit dem „Geheimen Clientschlüssel“ signiert worden. Den geheimen Clientschlüssel hat Live bei der Konfiguration der Anwendung angelegt (siehe Teil 2 – „Live ID Login“) und kann über das Managementportal https://manage.dev.live.com durch den Entwickler abgefragt werden.
Die Servicekommunikation steht nun vor der Aufgabe das JWT an den Service zu übertragen. Ein probates Mittel besteht darin, die Service-Security von Transport-Security auf TransportWithMessageCredential-Security umzustellen. Dies bedeutet, dass auf dem SSL verschlüsselten Kanal in der Nachricht eine User-Passwort Kombination übergeben wird.
Als Service-User wird die UserID des JWT genutzt. WCF nutzt wiederum den Namen und setzt diesen in der .NET-Umgebung als Name der PrimaryIdentity ein.

Autorisierung des WCF Services

Wie zuvor beschrieben wird das Binding auf TransportWithMessageCredential geändert:
Änderung des Bindings in der web.config

   1: <binding name="SayHelloServiceBinding">

   2:   <security mode="TransportWithMessageCredential">

   3:     <transport/>

   4:     <message clientCredentialType="UserName"/>

   5:   </security>

   6: </binding>

Durch die Konfiguration wird ausgesagt, dass im Nachrichteninhalt zusätzlich eine UserName und Passwort Kombination übertragen wird. Diese Informationen werden bei Empfang der Nachricht auf der Service-Seite überprüft, bevor der eigentliche Service aufgerufen wird. Die Service-Schnittstelle ändert sich somit nicht.
User und Passwort müssen auf Server-Seite überprüft werden. Netterweise findet sich in den LiveSDKSamples https://github.com/liveservices/LiveSDK ein Parser für das Format. Als Input nutzt der Parser das WebToken sowie den „geheimen Schlüssel“ zum Validieren der Signatur.
Der Service wird um die Klasse JsonWebToken erweitert:

Neue Klasse JsonWebToken.cs

   1: namespace WcfService

   2: {

   3:     // Reference: http://tools.ietf.org/search/draft-jones-json-web-token-00

   4:     //

   5:     // JWT is made up of 3 parts: Envelope, Claims, Signature.

   6:     // - Envelope - specifies the token type and signature algorithm used to produce 

   7:     //       signature segment. This is in JSON format

   8:     // - Claims - specifies claims made by the token. This is in JSON format

   9:     // - Signature - Cryptographic signature use to maintain data integrity.

  10:     // 

  11:     // To produce a JWT token:

  12:     // 1. Create Envelope segment in JSON format

  13:     // 2. Create Claims segment in JSON format

  14:     // 3. Create signature

  15:     // 4. Base64url encode each part and append together separated by "." 

  16:     using System;

  17:     using System.Collections.Generic;

  18:     using System.IO;

  19:     using System.Runtime.Serialization;

  20:     using System.Runtime.Serialization.Json;

  21:     using System.Security.Cryptography;

  22:     using System.Text;

  23:  

  24:     public class JsonWebToken

  25:     {

  26:         #region Helper Classes

  27:         [DataContract]

  28:         public class JsonWebTokenClaims

  29:         {

  30:             [DataMember(Name = "exp")]

  31:             private int expUnixTime

  32:             {

  33:                 get;

  34:                 set;

  35:             }

  36:             private DateTime? expiration = null;

  37:             public DateTime Expiration

  38:             {

  39:                 get

  40:                 {

  41:                     if (this.expiration == null)

  42:                     {

  43:                         this.expiration = new DateTime(1970, 1, 1, 0, 0, 0).AddSeconds(this.expUnixTime);

  44:                     }

  45:                     return (DateTime)this.expiration;

  46:                 }

  47:             }

  48:  

  49:             [DataMember(Name = "iss")]

  50:             public string Issuer

  51:             {

  52:                 get;

  53:                 private set;

  54:             }

  55:  

  56:             [DataMember(Name = "aud")]

  57:             public string Audience

  58:             {

  59:                 get;

  60:                 private set;

  61:             }

  62:  

  63:             [DataMember(Name = "uid")]

  64:             public string UserId

  65:             {

  66:                 get;

  67:                 private set;

  68:             }

  69:  

  70:             [DataMember(Name = "ver")]

  71:             public int Version

  72:             {

  73:                 get;

  74:                 private set;

  75:             }

  76:  

  77:             [DataMember(Name = "urn:microsoft:appuri")]

  78:             public string ClientIdentifier

  79:             {

  80:                 get;

  81:                 private set;

  82:             }

  83:  

  84:             [DataMember(Name = "urn:microsoft:appid")]

  85:             public string AppId

  86:             {

  87:                 get;

  88:                 private set;

  89:             }

  90:         }

  91:  

  92:         [DataContract]

  93:         public class JsonWebTokenEnvelope

  94:         {

  95:             [DataMember(Name = "typ")]

  96:             public string Type

  97:             {

  98:                 get;

  99:                 private set;

 100:             }

 101:  

 102:             [DataMember(Name = "alg")]

 103:             public string Algorithm

 104:             {

 105:                 get;

 106:                 private set;

 107:             }

 108:  

 109:             [DataMember(Name = "kid")]

 110:             public int KeyId

 111:             {

 112:                 get;

 113:                 private set;

 114:             }

 115:         }

 116:         #endregion

 117:  

 118:         #region Properties

 119:         private static readonly DataContractJsonSerializer ClaimsJsonSerializer = new DataContractJsonSerializer(typeof(JsonWebTokenClaims));

 120:         private static readonly DataContractJsonSerializer EnvelopeJsonSerializer = new DataContractJsonSerializer(typeof(JsonWebTokenEnvelope));

 121:         private static readonly UTF8Encoding UTF8Encoder = new UTF8Encoding(true, true);

 122:         private static readonly SHA256Managed SHA256Provider = new SHA256Managed();

 123:         private string claimsTokenSegment;

 124:  

 125:         public JsonWebTokenClaims Claims

 126:         {

 127:             get;

 128:             private set;

 129:         }

 130:  

 131:         private string envelopeTokenSegment;

 132:  

 133:         public JsonWebTokenEnvelope Envelope

 134:         {

 135:             get;

 136:             private set;

 137:         }

 138:  

 139:         public string Signature

 140:         {

 141:             get;

 142:             private set;

 143:         }

 144:  

 145:         public bool IsExpired

 146:         {

 147:             get

 148:             {

 149:                 return this.Claims.Expiration < DateTime.Now;

 150:             }

 151:         }

 152:         #endregion

 153:  

 154:         #region Constructors

 155:         public JsonWebToken(string token, Dictionary<int, string> keyIdsKeys)

 156:         {

 157:             // Get the token segments & perform validation

 158:             string[] tokenSegments = this.SplitToken(token);

 159:  

 160:             // Decode and deserialize the claims

 161:             this.claimsTokenSegment = tokenSegments[1];

 162:             this.Claims = this.GetClaimsFromTokenSegment(this.claimsTokenSegment);

 163:  

 164:             // Decode and deserialize the envelope

 165:             this.envelopeTokenSegment = tokenSegments[0];

 166:             this.Envelope = this.GetEnvelopeFromTokenSegment(this.envelopeTokenSegment);

 167:  

 168:             // Get the signature

 169:             this.Signature = tokenSegments[2];

 170:  

 171:             // Ensure that the tokens KeyId exists in the secret keys list

 172:             if (!keyIdsKeys.ContainsKey(this.Envelope.KeyId))

 173:             {

 174:                 throw new Exception(string.Format("Could not find key with id {0}", this.Envelope.KeyId));

 175:             }

 176:  

 177:             // Validation

 178:             this.ValidateEnvelope(this.Envelope);

 179:             this.ValidateSignature(keyIdsKeys[this.Envelope.KeyId]);

 180:         }

 181:  

 182:         private JsonWebToken()

 183:         {

 184:         }

 185:  

 186:         #endregion

 187:  

 188:         #region Parsing Methods

 189:         private JsonWebTokenClaims GetClaimsFromTokenSegment(string claimsTokenSegment)

 190:         {

 191:             byte[] claimsData = this.Base64UrlDecode(claimsTokenSegment);

 192:             using (MemoryStream memoryStream = new MemoryStream(claimsData))

 193:             {

 194:                 return ClaimsJsonSerializer.ReadObject(memoryStream) as JsonWebTokenClaims;

 195:             }

 196:         }

 197:  

 198:         private JsonWebTokenEnvelope GetEnvelopeFromTokenSegment(string envelopeTokenSegment)

 199:         {

 200:             byte[] envelopeData = this.Base64UrlDecode(envelopeTokenSegment);

 201:             using (MemoryStream memoryStream = new MemoryStream(envelopeData))

 202:             {

 203:                 return EnvelopeJsonSerializer.ReadObject(memoryStream) as JsonWebTokenEnvelope;

 204:             }

 205:         }

 206:  

 207:         private string[] SplitToken(string token)

 208:         {

 209:             // Expected token format: Envelope.Claims.Signature

 210:             if (string.IsNullOrEmpty(token))

 211:             {

 212:                 throw new Exception("Token is empty or null.");

 213:             }

 214:  

 215:             string[] segments = token.Split('.');

 216:             if (segments.Length != 3)

 217:             {

 218:                 throw new Exception("Invalid token format. Expected Envelope.Claims.Signature");

 219:             }

 220:             if (string.IsNullOrEmpty(segments[0]))

 221:             {

 222:                 throw new Exception("Invalid token format. Envelope must not be empty");

 223:             }

 224:             if (string.IsNullOrEmpty(segments[1]))

 225:             {

 226:                 throw new Exception("Invalid token format. Claims must not be empty");

 227:             }

 228:             if (string.IsNullOrEmpty(segments[2]))

 229:             {

 230:                 throw new Exception("Invalid token format. Signature must not be empty");

 231:             }

 232:             return segments;

 233:         }

 234:         #endregion

 235:  

 236:         #region Validation Methods

 237:         private void ValidateEnvelope(JsonWebTokenEnvelope envelope)

 238:         {

 239:             if (envelope.Type != "JWT")

 240:             {

 241:                 throw new Exception("Unsupported token type");

 242:             }

 243:             if (envelope.Algorithm != "HS256")

 244:             {

 245:                 throw new Exception("Unsupported crypto algorithm");

 246:             }

 247:         }

 248:  

 249:         private void ValidateSignature(string key)

 250:         {

 251:             // Derive signing key, Signing key = SHA256(secret + "JWTSig")

 252:             byte[] bytes = UTF8Encoder.GetBytes(key + "JWTSig");

 253:             byte[] signingKey = SHA256Provider.ComputeHash(bytes);

 254:  

 255:             // To Validate:

 256:             // 

 257:             // 1. Take the bytes of the UTF-8 representation of the JWT Claim

 258:             //  Segment and calculate an HMAC SHA-256 MAC on them using the

 259:             //  shared key.

 260:             //

 261:             // 2. Base64url encode the previously generated HMAC as defined in this

 262:             //  document.

 263:             //

 264:             // 3. If the JWT Crypto Segment and the previously calculated value

 265:             //  exactly match in a character by character, case sensitive

 266:             //  comparison, then one has confirmation that the key was used to

 267:             //  generate the HMAC on the JWT and that the contents of the JWT

 268:             //  Claim Segment have not be tampered with.

 269:             //

 270:             // 4. If the validation fails, the token MUST be rejected.

 271:  

 272:             // UFT-8 representation of the JWT envelope.claim segment

 273:             byte[] input = UTF8Encoder.GetBytes(this.envelopeTokenSegment + "." + this.claimsTokenSegment);

 274:  

 275:             // calculate an HMAC SHA-256 MAC

 276:             using (HMACSHA256 hashProvider = new HMACSHA256(signingKey))

 277:             {

 278:                 byte[] myHashValue = hashProvider.ComputeHash(input);

 279:  

 280:                 // Base64 url encode the hash

 281:                 string base64urlEncodedHash = this.Base64UrlEncode(myHashValue);

 282:  

 283:                 // Now compare the two has values

 284:                 if (base64urlEncodedHash != this.Signature)

 285:                 {

 286:                     throw new Exception("Signature does not match.");

 287:                 }

 288:             }

 289:         }

 290:         #endregion

 291:  

 292:         #region Base64 Encode / Decode Functions

 293:         // Reference: http://tools.ietf.org/search/draft-jones-json-web-token-00

 294:         public byte[] Base64UrlDecode(string encodedSegment)

 295:         {

 296:             string s = encodedSegment;

 297:             s = s.Replace('-', '+'); // 62nd char of encoding

 298:             s = s.Replace('_', '/'); // 63rd char of encoding

 299:             switch (s.Length % 4) // Pad with trailing '='s

 300:             {

 301:                 case 0: break; // No pad chars in this case

 302:                 case 2: s += "=="; break; // Two pad chars

 303:                 case 3: s += "="; break; // One pad char

 304:                 default: throw new System.Exception("Illegal base64url string");

 305:             }

 306:             return Convert.FromBase64String(s); // Standard base64 decoder

 307:         }

 308:  

 309:         public string Base64UrlEncode(byte[] arg)

 310:         {

 311:             string s = Convert.ToBase64String(arg); // Standard base64 encoder

 312:             s = s.Split('=')[0]; // Remove any trailing '='s

 313:             s = s.Replace('+', '-'); // 62nd char of encoding

 314:             s = s.Replace('/', '_'); // 63rd char of encoding

 315:             return s;

 316:         }

 317:         #endregion

 318:     }

 319: }

Jetzt fehlt nur noch die Überprüfung des Tokens.
Zur Erinnerung: Das Token enthält Claims und wurde von Live unterschrieben. Diese Unterschrift kann mit dem “geheimen Client Schlüssel” bzw. “Client Secret” der Anwendung geprüft werden. Wie im Teil 2 beschrieben, wird im Management Portal von Live der Schlüssel angezeigt bzw. neu erzeugt.
Die Prüfung des Tokens wird in einem UserNamePasswordValidator realisiert, der in die WCF Konfiguration eingehängt wird. Somit wird das transportierte Token überprüft ohne die Methodensignatur zu ändern. Der Code nutzt Funktionen aus System.IdentityModel bzw. System.IdentityModel.Selectors, die als Referenz hinzugefügt werden.

Neue Klasse CustomUserNameValidator.cs

   1: using System;

   2: using System.Collections.Generic;

   3: using System.IdentityModel.Selectors;

   4: using System.Linq;

   5: using System.ServiceModel;

   6: using System.Web;

   7:  

   8: namespace WcfService

   9: {

  10:     public class CustomUserNameValidator : UserNamePasswordValidator

  11:     {

  12:         const string LiveSecretClientKey = "ClientSecret_AusDemLivePortal";

  13:  

  14:         public override void Validate(string userName, string password)

  15:         {

  16:             try

  17:             {

  18:                 if (String.IsNullOrEmpty(userName) || String.IsNullOrEmpty(password))

  19:                 {

  20:                     throw new Exception("User or Password not set");

  21:                 }

  22:                 Dictionary<int, string> d = new Dictionary<int, string>();

  23:                 d.Add(0, LiveSecretClientKey);

  24:  

  25:                 // Parse Token und validiere Signatur

  26:                 JsonWebToken jwt = new JsonWebToken(password, d);

  27:  

  28:                 // Prüfe ob User mit der UserId im Token übereinstimmt

  29:                 var jwtUserName = jwt.Claims.UserId;

  30:                 if (jwtUserName != userName)

  31:                 {

  32:                     throw new Exception("Manipulated Username");

  33:                 }

  34:             }

  35:             catch (Exception e)

  36:             {

  37:                 FaultException fe = new FaultException("Unknown Username or Incorrect Password 
" + e.ToString());

  38:                 throw fe;

  39:             }

  40:         }

  41:     }

  42: }

Der geheime Client Schlüssel muss im CustomUserNameValidator eingetragen werden (siehe Zeile 12) damit dieser die Unterschrift prüfen kann.
Das Einhängen des Validators erfolgt in einer Behavior-Konfiguration des Services.

Konfiguration des CustomUserNameValidators in der web.config

   1: <behavior>

   2:   <serviceBehaviors>

   3:     [...]

   4:     <serviceCredentials>

   5:       <userNameAuthentication 

   6:         userNamePasswordValidationMode="Custom"

   7:         customUserNamePasswordValidatorType="WcfService.CustomUserNameValidator, WcfService" />

   8:     </serviceCredentials>          

   9:   </behavior>

  10: </serviceBehaviors>

Zum Debuggen des Validators muss der Debugger an den IISExpress Attached werden.

Setzen der Service-Credentials im Client

Nun muss der Client beim Serviceaufruf User und Passwort der Service-Authentifizierung setzen. Ansonsten erhält man nachfolgende Fehlermeldung:
InvalidOperationException „The username is not provided. Specify username in ClientCredentials“
Im ersten Schritte wird die UserID aus dem JWT Token extrahiert werden. Die Extraktion erfolgt dabei mit dem gleichen Code wie im CustomUserNameValiditor, mit Ausnahme der Validierung. Zum einen muss auf der Client-Seit das Token nicht validiert werden, zum anderen stehen auch einige kryptografische Funktionen in WinRT nicht zur Verfügung, so dass der Code nur leicht modifiziert funktioniert.

Neu Klasse JsonWebTokenMin.cs

   1: namespace ClientStoraApp

   2: {

   3:     // Reference: http://tools.ietf.org/search/draft-jones-json-web-token-00

   4:     //

   5:     // JWT is made up of 3 parts: Envelope, Claims, Signature.

   6:     // - Envelope - specifies the token type and signature algorithm used to produce 

   7:     //       signature segment. This is in JSON format

   8:     // - Claims - specifies claims made by the token. This is in JSON format

   9:     // - Signature - Cryptographic signature use to maintain data integrity.

  10:     // 

  11:     // To produce a JWT token:

  12:     // 1. Create Envelope segment in JSON format

  13:     // 2. Create Claims segment in JSON format

  14:     // 3. Create signature

  15:     // 4. Base64url encode each part and append together separated by "." 

  16:  

  17:     using System;

  18:     using System.Collections.Generic;

  19:     using System.IO;

  20:     using System.Runtime.Serialization;

  21:     using System.Runtime.Serialization.Json;

  22:     using System.Text;

  23:  

  24:     public class JsonWebTokenMin

  25:     {

  26:         #region Helper Classes

  27:         [DataContract]

  28:         public class JsonWebTokenClaims

  29:         {

  30:             [DataMember(Name = "exp")]

  31:             private int expUnixTime

  32:             {

  33:                 get;

  34:                 set;

  35:             }

  36:             private DateTime? expiration = null;

  37:             public DateTime Expiration

  38:             {

  39:                 get

  40:                 {

  41:                     if (this.expiration == null)

  42:                     {

  43:                         this.expiration = new DateTime(1970, 1, 1, 0, 0, 0).AddSeconds(this.expUnixTime);

  44:                     }

  45:                     return (DateTime)this.expiration;

  46:                 }

  47:             }

  48:  

  49:             [DataMember(Name = "iss")]

  50:             public string Issuer

  51:             {

  52:                 get;

  53:                 private set;

  54:             }

  55:  

  56:             [DataMember(Name = "aud")]

  57:             public string Audience

  58:             {

  59:                 get;

  60:                 private set;

  61:             }

  62:  

  63:             [DataMember(Name = "uid")]

  64:             public string UserId

  65:             {

  66:                 get;

  67:                 private set;

  68:             }

  69:  

  70:             [DataMember(Name = "ver")]

  71:             public int Version

  72:             {

  73:                 get;

  74:                 private set;

  75:             }

  76:  

  77:             [DataMember(Name = "urn:microsoft:appuri")]

  78:             public string ClientIdentifier

  79:             {

  80:                 get;

  81:                 private set;

  82:             }

  83:  

  84:             [DataMember(Name = "urn:microsoft:appid")]

  85:             public string AppId

  86:             {

  87:                 get;

  88:                 private set;

  89:             }

  90:         }

  91:  

  92:         [DataContract]

  93:         public class JsonWebTokenEnvelope

  94:         {

  95:             [DataMember(Name = "typ")]

  96:             public string Type

  97:             {

  98:                 get;

  99:                 private set;

 100:             }

 101:  

 102:             [DataMember(Name = "alg")]

 103:             public string Algorithm

 104:             {

 105:                 get;

 106:                 private set;

 107:             }

 108:  

 109:             [DataMember(Name = "kid")]

 110:             public int KeyId

 111:             {

 112:                 get;

 113:                 private set;

 114:             }

 115:         }

 116:         #endregion

 117:  

 118:         #region Properties

 119:         private static readonly DataContractJsonSerializer ClaimsJsonSerializer = new DataContractJsonSerializer(typeof(JsonWebTokenClaims));

 120:         private static readonly DataContractJsonSerializer EnvelopeJsonSerializer = new DataContractJsonSerializer(typeof(JsonWebTokenEnvelope));

 121:         private static readonly UTF8Encoding UTF8Encoder = new UTF8Encoding(true, true);

 122:         private string claimsTokenSegment;

 123:  

 124:         public JsonWebTokenClaims Claims

 125:         {

 126:             get;

 127:             private set;

 128:         }

 129:  

 130:         private string envelopeTokenSegment;

 131:  

 132:         public JsonWebTokenEnvelope Envelope

 133:         {

 134:             get;

 135:             private set;

 136:         }

 137:  

 138:         public string Signature

 139:         {

 140:             get;

 141:             private set;

 142:         }

 143:  

 144:         public bool IsExpired

 145:         {

 146:             get

 147:             {

 148:                 return this.Claims.Expiration < DateTime.Now;

 149:             }

 150:         }

 151:         #endregion

 152:  

 153:         #region Constructors

 154:         public JsonWebTokenMin(string token)

 155:         {

 156:             // Get the token segments & perform validation

 157:             string[] tokenSegments = this.SplitToken(token);

 158:  

 159:             // Decode and deserialize the claims

 160:             this.claimsTokenSegment = tokenSegments[1];

 161:             this.Claims = this.GetClaimsFromTokenSegment(this.claimsTokenSegment);

 162:  

 163:             // Decode and deserialize the envelope

 164:             this.envelopeTokenSegment = tokenSegments[0];

 165:             this.Envelope = this.GetEnvelopeFromTokenSegment(this.envelopeTokenSegment);

 166:  

 167:             // Get the signature

 168:             this.Signature = tokenSegments[2];

 169:  

 170:             // Validation

 171:             this.ValidateEnvelope(this.Envelope);

 172:         }

 173:  

 174:         private JsonWebTokenMin()

 175:         {

 176:         }

 177:         #endregion

 178:  

 179:         #region Parsing Methods

 180:  

 181:         private JsonWebTokenClaims GetClaimsFromTokenSegment(string claimsTokenSegment)

 182:         {

 183:             byte[] claimsData = this.Base64UrlDecode(claimsTokenSegment);

 184:             using (MemoryStream memoryStream = new MemoryStream(claimsData))

 185:             {

 186:                 return ClaimsJsonSerializer.ReadObject(memoryStream) as JsonWebTokenClaims;

 187:             }

 188:         }

 189:  

 190:         private JsonWebTokenEnvelope GetEnvelopeFromTokenSegment(string envelopeTokenSegment)

 191:         {

 192:             byte[] envelopeData = this.Base64UrlDecode(envelopeTokenSegment);

 193:             using (MemoryStream memoryStream = new MemoryStream(envelopeData))

 194:             {

 195:                 return EnvelopeJsonSerializer.ReadObject(memoryStream) as JsonWebTokenEnvelope;

 196:             }

 197:         }

 198:  

 199:         private string[] SplitToken(string token)

 200:         {

 201:             // Expected token format: Envelope.Claims.Signature

 202:             if (string.IsNullOrEmpty(token))

 203:             {

 204:                 throw new Exception("Token is empty or null.");

 205:             }

 206:             string[] segments = token.Split('.');

 207:             if (segments.Length != 3)

 208:             {

 209:                 throw new Exception("Invalid token format. Expected Envelope.Claims.Signature");

 210:             }

 211:             if (string.IsNullOrEmpty(segments[0]))

 212:             {

 213:                 throw new Exception("Invalid token format. Envelope must not be empty");

 214:             }

 215:             if (string.IsNullOrEmpty(segments[1]))

 216:             {

 217:                 throw new Exception("Invalid token format. Claims must not be empty");

 218:             }

 219:             if (string.IsNullOrEmpty(segments[2]))

 220:             {

 221:                 throw new Exception("Invalid token format. Signature must not be empty");

 222:             }

 223:             return segments;

 224:         }

 225:         #endregion

 226:  

 227:         #region Validation Methods

 228:         private void ValidateEnvelope(JsonWebTokenEnvelope envelope)

 229:         {

 230:             if (envelope.Type != "JWT")

 231:             {

 232:                 throw new Exception("Unsupported token type");

 233:             }

 234:             if (envelope.Algorithm != "HS256")

 235:             {

 236:                 throw new Exception("Unsupported crypto algorithm");

 237:             }

 238:         }

 239:         #endregion

 240:  

 241:         #region Base64 Encode / Decode Functions

 242:         // Reference: http://tools.ietf.org/search/draft-jones-json-web-token-00

 243:         public byte[] Base64UrlDecode(string encodedSegment)

 244:         {

 245:             string s = encodedSegment;

 246:             s = s.Replace('-', '+'); // 62nd char of encoding

 247:             s = s.Replace('_', '/'); // 63rd char of encoding

 248:             switch (s.Length % 4) // Pad with trailing '='s

 249:             {

 250:                 case 0: break; // No pad chars in this case

 251:                 case 2: s += "=="; break; // Two pad chars

 252:                 case 3: s += "="; break; // One pad char

 253:                 default: throw new System.Exception("Illegal base64url string");

 254:             }

 255:             return Convert.FromBase64String(s); // Standard base64 decoder

 256:         }

 257:  

 258:         public string Base64UrlEncode(byte[] arg)

 259:         {

 260:             string s = Convert.ToBase64String(arg); // Standard base64 encoder

 261:             s = s.Split('=')[0]; // Remove any trailing '='s

 262:             s = s.Replace('+', '-'); // 62nd char of encoding

 263:             s = s.Replace('/', '_'); // 63rd char of encoding

 264:             return s;

 265:         }

 266:         #endregion

 267:     }

 268: }

Als Service-Passwort wird das JWT Token direkt genutzt. Service-User und Service-Passwort können nun zum Service Aufruf (siehe Zeile 14-15) hinzugefügt werden.

Änderung des WCF Aufrufs MainPage.xaml.cs

   1: private async void CallWCFButton_Click(object sender, RoutedEventArgs e)

   2: {

   3:     SayHelloServiceClient helloClient = new SayHelloServiceClient(

   4:         SayHelloServiceClient.EndpointConfiguration.SayHelloService);

   5:     // Endpunkt für IIS Express (Portnummer bitte anpassen) 

   6:     // EndpointAddress ea = new EndpointAddress("https://localhost:44303/SayHelloService.svc");

   7:     // Alternativer Endpunkt für Azure (Rechnername und Portnummer bitte anpassen)

   8:     //EndpointAddress ea = new EndpointAddress("https://win8securewcf.cloudapp.net:443/SayHelloService.svc");

   9:     helloClient.Endpoint.Address = ea;

  10:  

  11:     if (! String.IsNullOrEmpty(AuthToken))

  12:     {

  13:         JsonWebTokenMin jwt = new JsonWebTokenMin(AuthToken);

  14:         helloClient.ClientCredentials.UserName.UserName = jwt.Claims.UserId;

  15:         helloClient.ClientCredentials.UserName.Password = AuthToken;

  16:     }

  17:     WcfOutput = await helloClient.SayHelloAsync("lieber Benutzer");

  18: }

Wird nun der Client gestartet antwortet der Service korrekt. Mit einer kleinen Erweiterungen gibt der Service auch die UserId mit aus.

Änderung an SayHello.cs

   1: public string SayHello(string input)

   2: {

   3:   ServiceSecurityContext ssc = OperationContext.Current.ServiceSecurityContext;

   4:   string userName = ssc.PrimaryIdentity.Name;

   5:   return String.Format("Hello {0}. Your JWT-UserID is '{1}' ", input, userName);

   6: }

Finale

Knackpunkt in diesem Teil war die Auswertung und Validierung des Json Web Tokens. Dieses wird durch den Client bereitgestellt und im ServiceHost durch den Validator geprüft.