Calling a WCF Web Service Method
Register Application with Software Potential
Calls to the Software Potential web services must be authenticated via OAuth Client Credentials Flow. You must first register your application as a client with Software Potential as detailed in the document Register client Application With Software Potential.
You will need to copy the following values from the client configuration in Software Potential:
- Client ID
- Client Secret
- Scopes: To call the WCF services the scopes must include the "wcf" scope
Add a Web Service reference
In the Visual Studio project select Add -> Service Reference and enter the following endpoint address "https://srv.softwarepotential.com/SLMServerWS/LicenseManagementBearerWS.svc".
For legacy application that authenticate using credentials of a Software Potential account, the endpoint address is "https://srv.softwarepotential.com/SLMServerWS/LicenseManagementWS.svc".
Create Client
Create a LicenseManagementApi class to wrap the LicenseManagementWS client (see the LicenseManagementApi section below for more details).
Pass the application's ClientId and ClientSecret, together with the required Scope(s) as registered with Software Potential.
var serviceCredentials = GetServiceCredentials(); //Read credentials from configuration setting
var api = LicenseManagementApiFactory.Create(
serviceCredentials.ClientId,
serviceCredentials.ClientSecret,
serviceCredentials.Scope );
When authenticating with valid Software Potential account credentials
var serviceCredentials = GetServiceCredentials(); //Read credentials from configuration setting
var api = LicenseManagementApiFactory.Create(
serviceCredentials.Username,
serviceCredentials.Password);
Call a Method
Execute the call to the web service method via a lambda in LicenseManagementApi.Execute(). For example, to retrieve the available products named "Console Application" in Software Potential:
static Product[] GetProducts(LicenseManagementApi api) { try { return api.Execute(client => { return client.GetProducts().Where(p => p.Name.Contains("Console Application")).ToArray(); }); } catch (Exception ex) // NB: Execute already handles error messages, but throws the exception on { Console.WriteLine(ex.StackTrace); return null; } }
It is also possible to wrap multiple web service method calls in a single call to LicenseManagementApi. For example to issue a license based on a SKU:
static License IssueLicenseBySku(LicenseManagementApi api, string skuId) { try { return api.Execute(client => { var sku = client.GetSkuById(skuId); return client.IssueLicense(sku.LicenseInfo); }); } catch (Exception ex) // NB: Execute already handles error messages, but throws the exception on { Console.WriteLine(ex.StackTrace); return null; } }
LicenseManagementApi Class
It is recommend that developers use the LicenseManagementApi class to wrap, and execute methods on, the web service client proxy. This class correctly handles communications exceptions that can fault the client proxy and prevent creation of a new proxy client.
The following values must be copied from the client configuration in Software Potential:
- Client Id
- Client Secret
- Scopes
- Authority - this needs to be set to "https://sts.softwarepotential.com" for all WCF clients
In the following snippets these values are read from an app configuration file.
Bearer token Authentication
The following snippet illustrates a sample LicenseManagementApi class when authenticating with a Bearer token:
// Allows correct execution of multiple API calls even if exceptions occur // (which makes the LicenseManagementWSClient faulted, in which case a fresh one needs to be generated) // Enables one to have a single place in the code to load the credentials from e.g. a config file static class LicenseManagementApiFactory { public static LicenseManagementApi Create(string clientId, string clientSecret, string scope) { return new LicenseManagementApi(() => InternalCreateRaw(clientId, clientSecret, scope)); } public static ILicenseManagementWS InternalCreateRaw( string clientId, string clientSecret, string scope ) { //TLS1.2 is required ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; string _url = SpApiConfiguration.BaseUrl + "SLMServerWS/LicenseManagementBearerWS.svc"; var token = JwtTokenHelper.GetWrappedAccessToken( clientId, clientSecret, scope ); var binding = new WS2007FederationHttpBinding( WSFederationHttpSecurityMode.TransportWithMessageCredential ); binding.HostNameComparisonMode = HostNameComparisonMode.Exact; binding.Security.Message.EstablishSecurityContext = false; binding.Security.Message.IssuerAddress = new EndpointAddress( SpApiConfiguration.Authority ); binding.Security.Message.IssuedKeyType = SecurityKeyType.BearerKey; binding.MaxReceivedMessageSize = 2147483647; //var factory = new ChannelFactory(binding, new EndpointAddress(_url)); var factory = new ChannelFactory( binding, new EndpointAddress( _url ) ); return factory.CreateChannelWithIssuedToken( token ); } } class LicenseManagementApi { readonly Func _createClient; public LicenseManagementApi( Func createClient ) { _createClient = createClient; } public TResult Execute(Func<ILicenseManagementWS, TResult> serviceCalls) { var client = _createClient(); try { return serviceCalls( client ); } catch (Exception ex) { Console.WriteLine("LICENSE MANAGMENT API EXCEPTION: " + ex); ( ( IClientChannel )client ).Abort(); throw; } finally { ( ( IClientChannel )client ).IfNotFaultedCloseAndCleanupChannel(); } } } static class WcfExtensions { /// /// Safely closes a service client connection. /// ///The client connection to close. public static void IfNotFaultedCloseAndCleanupChannel(this ICommunicationObject client) { // Don't try to Close if we are Faulted - this would cause another exception which would hide the primary one if (client.State == CommunicationState.Opened) try { // Close this client client.Close(); } catch ( CommunicationException ) { client.Abort(); } catch ( TimeoutException ) { client.Abort(); } catch ( Exception ) { client.Abort(); throw; } } }
Get Access Token
You will need to get an Access Token from the Software Potential STS and wrap this appropriately as a SAML token to be included in the request. The following snippet shows one approach to this using a custom helper class to get a new token for each call as is done in the previous snippet. If a large number of requests are to be submitted, the token should instead be cached and refreshed as required.
public static class JwtTokenHelper { public static GenericXmlSecurityToken GetWrappedAccessToken(string clientId, string clientSecret, string scope) { var token = GetAccessToken(clientId, clientSecret, scope); return WrapJwt(token); } private static string GetAccessToken(string clientId, string clientSecret, string scope) { var client = new HttpClient(); var disco = client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest { Address = SpApiConfiguration.Authority.ToLower(), Policy = { RequireHttps = false } }).Result; if (disco.IsError) throw new Exception(disco.Error); var tokenResponse = client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = disco.TokenEndpoint, ClientId = clientId, ClientSecret = clientSecret, Scope = scope }).Result; if (tokenResponse.IsError) throw new Exception(tokenResponse.Error); return tokenResponse.AccessToken; } private static GenericXmlSecurityToken WrapJwt(string jwt) { var subject = new ClaimsIdentity("saml"); subject.AddClaim(new Claim(nameof(jwt), jwt)); var descriptor = new SecurityTokenDescriptor { TokenType = "urn:oasis:names:tc:SAML:2.0:assertion", TokenIssuerName = "urn:wrappedjwt", Subject = subject }; var handler = new Saml2SecurityTokenHandler(); var jwttoken = handler.CreateToken(descriptor); var xml = jwttoken.ToTokenXmlString(); var xelement = ToXmlElement(XElement.Parse(xml)); var xmlToken = new GenericXmlSecurityToken(xelement, null, DateTime.UtcNow, DateTime.Now.AddHours(1), null, null, null); return xmlToken; } public static XmlElement ToXmlElement(XElement el) { var doc = new XmlDocument(); doc.Load(el.CreateReader()); return doc.DocumentElement; } }
Legacy Credentials-based Authentication
The following snippet illustrates how to create the LicenseManagementApi class when using Software Potential account credentials
// Allows correct execution of multiple API calls handling exceptions // (when LicenseManagementWSClient is faulted a fresh client needs to be generated) // Also provides a single place in the code to load the credentials from e.g. a config file static class LicenseManagementApiFactory { public static LicenseManagementApi Create() { var credentials = GetCredentials(); //Read credentials from configuration file return new LicenseManagementApi(() => InternalCreateRaw(credentials.Username, credentials.Password)); } public static LicenseManagementApi Create(string username, string password) { return new LicenseManagementApi(() => InternalCreateRaw(username, password)); } public static LicenseManagementWSClient InternalCreateRaw(string username, string password) { var client = new LicenseManagementWSClient( "WSHttpBinding_ILicenseManagementWS", "https://srv.softwarepotential.com/SLMServerWS/LicenseManagementWS.svc"); var clientCreds = client.ClientCredentials.UserName; clientCreds.UserName = username; clientCreds.Password = password; return client; } } // Safe wrapper that manages cleaning up WCF proxies correctly class LicenseManagementApi { readonly Func _createClient; public LicenseManagementApi(Func createClient) { _createClient = createClient; } public TResult Execute(Func<LicenseManagementWSClient, TResult> serviceCalls) { var client = _createClient(); try { return serviceCalls(client); } catch (Exception ex) { client.Abort(); throw; } finally { client.IfNotFaultedCloseAndCleanupChannel(); } } public void Execute(Action<LicenseManagementWSClient> serviceCalls) { var client = _createClient(); try { return serviceCalls(client); } catch (Exception ex) { client.Abort(); throw; } finally { client.IfNotFaultedCloseAndCleanupChannel(); } } } static class WcfExtensions { /// <summary> /// Safely closes a service client connection. /// </summary> /// <param name="client">The client connection to close.</param> public static void IfNotFaultedCloseAndCleanupChannel(this ICommunicationObject client) { // Don't try to Close if we are Faulted - this would cause another exception which would hide the primary one if (client.State == CommunicationState.Opened) try { // Close this client client.Close(); } catch (CommunicationException) { client.Abort(); } catch (TimeoutException) { client.Abort(); } catch (Exception) { client.Abort(); throw; } } } }
Comments
0 comments
Article is closed for comments.