Thursday, March 30, 2017

WCF : Calling an async. method

Just for a change, I was asked to help out on a non-Identity project.

There was a legacy host that only understood SOAP and a modern back-end that only supported REST web API.

So we need a bridge between them and I had to remember everything I had ever forgotten about WCF.

I needed my WCF method to call:

string response = await my_api.CallREST(parameter);

The problem is that the compiler expects the method to be decorated with async.

Aync. WCF?

Turns out you can.

My method looks like:

public async Task<validateresponse> ValidateParameter (parameter)

... and similarly for the interface.

And it works!

The word "async" doesn't appear anywhere is the WSDL. It appears to be completely ignored.

Enjoy!

Friday, March 24, 2017

ADFS : Copy claims rules over

Don't know how many times I've done this.

Deploy to Dev., get everything working, deploy to QAS, support QAS acceptance testing, deploy to Prod., smoke test.

When you have a lot of claims rules to copy over, I found a neat way to do it.

e.g. for a CP.

(Get-AdfsClaimsProviderTrust -Name "My CP").AcceptanceTransformRules | Out-File “C:\path\CPClaimsRules.txt”

And then import the rules to the new CP.

Set-AdfsClaimsProviderTrust -TargetName "My CP" -AcceptanceTransformRulesFile “C:\path\CPClaimsRules.txt” .

I've found that this also avoids the issue where you keep a copy of the rules in e.g. Word and then when you try and paste them into the ADFS wizard, you get all kinds of format errors.

Enjoy!

Wednesday, March 22, 2017

ADFS : Creating a custom attribute store

This is for ADFS 4.0 on Server 2016.

This is a good write-up.

Unfortunately, all the code is in a screen shot which sucks. Somewhat difficult to copy / paste :-).

Luckily, similar code can be found here.

Just standardise the names; one is "ToUpper"; the other is "toUpper".

However, my requirement was for getting claims from a back-end (details unimportant for the purposes of this post) where a user could have many claims of that type returned. Think of a property ID where one person owns a house but an investor owns several.

All the examples were for returning one attribute.

Essentially, you are returning a C# jagged array e.g. string[][] resultData.

My query string was :

c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]
 => issue(store = "CAS", types = ("HouseID"), query = "House", param = c.Value);

So I assumed that the multiple claims would be in the same row i.e. one row; many columns.

The search to the back-end returned 3 houses.

I then got:

Microsoft.IdentityServer.ClaimsPolicy.Language.PolicyEvaluationException: POLICY0019: Query 'House' to attribute store 'CAS' returned an unexpected number of fields: expected '1', got '3'.

If you look at the query, you'll see there is only one query parameter which will only return one result. A query which returns 3 results would be like:

query = "House", sn, mail

So what I need is 3 rows; one column.

I found some guidance around this here.

One of the things that stumped me for a while was the fact that the array had to be dynamic because there could be any number of houses. That's why you add to a list and then cast to an array.

 Another gotcha was the fact that while you can have a static rule like:

=> issue(type = "HouseID", value = "123456");

you cannot have that in a query string for the attribute store. You get:

System.ArgumentException: ID4216: The ClaimType 'HouseID' must be of format 'namespace'/'name'.

So it needs to be:

c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]
 => issue(store = "CAS", types = ("http://claim/HouseID"), query = "House", param = c.Value);

i.e. HouseID becomes http://claim/HouseID.

Once you have compiled a new .dll file, you have to copy it over in the ADFS directory on the server.

You will get "Access Denied" because ADFS is running. So you have to stop the ADFS service, copy over the .dll and then start up the service again.

You do not have to delete the custom attribute store in the wizard and reload it. When ADFS starts. it will load the latest .dll.

The gist with the code is here.

But if you want a sneak preview, the important part is:

try
{
    // Dummy values to illustrate the principle.

    List<string> claimValues = new List<string>();
    claimValues.Add("123456");
    claimValues.Add("654321");
    claimValues.Add("456123");
                
    List<string[]> claimData = new List<string[]>();

    // Each claim value is added to its own string array 
    foreach (string claimVal in claimValues)
    {
        claimData.Add(new string[1] { claimVal });
    }

    // The claim value string arrays are added to the string [][] that is 
    // returned by the Custom Attribute Store EndExecuteQuery()
    string[][] resultData = claimData.ToArray();

    TypedAsyncResult<string[][]> asyncResult = new TypedAsyncResult<string[]
    []>(callback, state);
    asyncResult.Complete(resultData, true);
    return asyncResult;
}

catch (Exception ex)
{
    String innerMess = "";
    if (ex.InnerException != null)
        innerMess = ex.InnerException.ToString();
    throw new AttributeStoreQueryExecutionException("CAS exception : " +
       ex.Message + " " + innerMess);
}
 

Enjoy!

Tuesday, March 21, 2017

NLOG : Logging for both web site and web API

I have a standard .NET 4.5 MVC web site with web API.

I added NLOG to the web API via the NuGet packages and log away quite happily.

Part of the NuGet are the files:
  • NLog.config
  • NLog.xsd
I then wanted to do logging in the web site. So add the NuGet to that. Added quite happily but no NLog files?

So I copied the two NLog files from the web API project and pasted them into the web site project.

I changed the logging file name. So I have web-site.log and web-api.log.

Works fine.

Enjoy!

Monday, March 20, 2017

ADFS : WIF10201: No valid key mapping found

The error is:

WIF10201: No valid key mapping found for securityToken: 'System.IdentityModel.Tokens.X509SecurityToken' and issuer: 'http://MY-ADFS/adfs/services/trust'.

I have a simple WIF application circa VS 2012 that I use to display claims and ported it over to use on ADFS 4.0.

Then I got the above error.

The solution is as per Signing key rollover in Azure Active Directory.

Yes - it says AAD but the client-side code for ADFS is the same since it's all driven from the metadata.

Use the code from: "Web applications protecting resources and created with Visual Studio 2012".

When I compared the web.config changes, the error seemed to be because the server name is "MY-ADFS" (in caps) but I had written "my-adfs" (no caps) in the web.config.

The thumbprint was also in caps. (Although I've never had an issue with that).

It gives you a nice comment:

"Element below commented by: ValidatingIssuerNameRegistry.WriteToConfg on: '20/03/2017 1:00:16 a.m. (UTC)'. Differences were found in the Metadata from: ..."

Enjoy!

AAD : Beware the difference in the V1.0 and V2.0 endpoints

The V2.0 endpoint is the endpoint that allows you to sign in with the converged Microsoft and Azure Active Directory accounts.

From an OpenID / OAuth perspective, the discovery documents can be found at:

V1.0:

https://login.microsoftonline.com/common/.well-known/openid-configuration

V2.0:

https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration

Notice the extra "V2.0".

If we look at the keys, we see for V1.0:

https://login.microsoftonline.com/common/discovery/keys

And for V2.0 :

https://login.microsoftonline.com/common/discovery/v2.0/keys

You will notice that V2.0 has four signing keys while v1.0 only has two.

V2.0 is the endpoint used by B2C.

Don't assume that tokens signed by Azure AD (V1.0) are also acceptable for B2C (V2.0) and vice versa.

This will be true when AAD and B2C are merged at some point in the future but right now it's a gotcha!

Enjoy!

Wednesday, March 15, 2017

ADFS : Using the client_credentials flow with ADFS 4.0 returns 401

Been battling with this for ages. I'm using ADFS 4.0 on Server 2016.

My web site uses OpenID Connect and that uses the OWIN authorisation code grant.

When I use the authorisation code grant,  I can get the code, exchange the code for an access token and then call a web API with that access token no problem.

But that flow requires a user to authenticate and for some of my use cases there is no user. An example would be a forgotten password flow where the user cannot authenticate. To do that, I use the client_credentials flow.

Getting this to work was a non-trivial task since the documentation is (shall we say) sub optimal.

I also needed the ADAL Nuget package. Interesting that you can mix OWIN and ADAL

So under "Application Groups", I have the "Server application accessing a web API" scenario.

The server application has a client ID and the secret key.

So assume the general API URL is:

https://my-pc/WebService

And the API's inside this are like:

https://my-pc/WebService/api/my-api/

The web API in the wizard has the RP identifier:

https://my-pc/WebService

(This matches the ValidAudience below).

Access control policy is:

Permit everyone

I have one custom claim rule:

c:[] => issue(claim = c);

Now there is no way to pass these claims back in the JWT but I added this during my troubleshooting.

Client permissions is set to:

"All clients"

with scope of:

openid and user_impersonation.

The web API controller is decorated with  [Authorize].

The code is:

using Microsoft.IdentityModel.Clients.ActiveDirectory;

ClientCredential clientCredential = new ClientCredential(clientId, secretKey);

AuthenticationContext ac = new AuthenticationContext("https://my-adfs/adfs/", false);
AuthenticationResult result = await ac.AcquireTokenAsync("https://my-pc/WebService"
clientCredential);

HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post,  
"https://my-pc/WebService/api/my-api/");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

HttpContent content = new FormUrlEncodedContent(new[] { new KeyValuePair<string,  
string>("foo", "blah"), new KeyValuePair<string, string>("foo1", "blah1") });
request.Content = content;
HttpResponseMessage response = await client.SendAsync(request);

if (response.IsSuccessStatusCode)
    // etc

To call a second API, it's the same code with one change:

HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "https://my-pc/WebService/api/my-api1/");

I also has to change to code for the web API App_Start, Startup.Auth.cs.

app.UseActiveDirectoryFederationServicesBearerAuthentication(
    new ActiveDirectoryFederationServicesBearerAuthenticationOptions
    {
        TokenValidationParameters = new TokenValidationParameters()
        {
            SaveSigninToken = true,
            ValidAudience = "https://my-pc/WebService"
        },
        
        MetadataEndpoint = "https://my-adfs/FederationMetadata/2007-06/FederationMetadata.xml"
});

Enjoy!

Monday, March 06, 2017

ADFS : Health Check

This is a question I've been asked a number of times.

Usually, you just ping the metadata endpoint or the IDPInitiatedSignOn endpoint.

Then I found: AD FS Diagnostics Module.

"The AD FS Diagnostics Module contains commandlets to gather configuration information of an AD FS server, as well as commandlets to perform health checks to detect configuration issues based on common root causes identified during support engagements such as duplicate SPN, cert".

There are some other useful links on the LHS.

On my list to try out :-)

Enjoy!

Tuesday, February 14, 2017

AD : Using PowerShell to create users and groups

ADFS specifically targets authentication and authorisation.

It does not target provisioning users into AD or adding them to groups. You need an Identity Manager for that.

You can do this with PowerShell and there are many links to do this around on the Internet e.g. PowerShell: Bulk create AD Users from CSV file.

My version

csv file format is:

Firstname,Lastname,Maildomain,SAM,OU,Password,Email
Joe,Bloggs,mydomain.local,jbloggs,"OU=MyUsers,DC=mydomain,DC=local",password,jb@abc.co.nz

Script

$Users = Import-Csv -Path "C:\blah\Users.csv"            
foreach ($User in $Users)            
{            
    $Displayname = $User.'Firstname' + " " + $User.'Lastname'            
    $UserFirstname = $User.'Firstname'            
    $UserLastname = $User.'Lastname'            
    $OU = $User.'OU'            
    $SAM = $User.'SAM'            
    $UPN = $User.'Firstname' + "." + $User.'Lastname' + "@" + $User.'Maildomain'            
    $Password = $User.'Password'  
    $Email = $User.'Email'
    
    New-ADUser -Name "$Displayname" -DisplayName "$Displayname" -SamAccountName $SAM -UserPrincipalName $UPN -GivenName "$UserFirstname" -Surname "$UserLastname" -Description "$Description" -AccountPassword (ConvertTo-SecureString $Password -AsPlainText -Force) -EmailAddress "$Email" -Enabled $true -Path "$OU" -ChangePasswordAtLogon $false –PasswordNeverExpires $true -server mydomain.local 

    Write-Host –NoNewLine "Adding user:  "
    Write-Host $SAM
}
 
$Users = Import-Csv -Path "C:\blah\Users.csv"            
foreach ($User in $Users)            
{   
    $SAM = $User.'SAM'            
    
    Add-ADGroupMember -Identity "My Group 1" -Member $SAM
    Add-ADGroupMember -Identity "My Group 2" -Member $SAM
    
    Write-Host –NoNewLine "Adding groups for user:  "
    Write-Host $SAM
}    

Enjoy!

Wednesday, February 08, 2017

ADFS : Useful PowerShell cmdlets

I seem to be running these on just about every installation I do these days so thought it would be worthwhile to note them.

Login with email address

This seems to be more and more common.

Set-AdfsClaimsProviderTrust -TargetIdentifier "AD AUTHORITY" -AlternateLoginID mail -LookupForests company.co.nz 

There are some gotchas with this especially if you are thinking of extending out to Azure (via AD Connect) at some point.

Configuring Alternate Login ID

Certificate revocation

Most Dev. instances don't have access to the extranet. The ADFS login will be slower because ADFS will try and check for certificate revocation.

So it makes sense to remove this functionality.

Set-AdfsRelyingPartyTrust -TargetIdentifier urn:xxx -SigningCertificateRevocationCheck None -EncryptionCertificateRevocationCheck None

Skew

Although the time across servers should be consistent, in a lot of cases it isn't. This means that if the ADFS server is ahead, the SAML token will be in the future and the SAML RP will reject it.

Some SAML RP that I have dealt with have the skew hard coded so it cannot be altered.

The best solution is to ensure that the server time is synchronised but if that is not possible, you can "back date" the time in the token. The cmdlet below sets the time 3 minutes backwards.

Set-AdfsRelyingPartyTrust -TargetIdentifier urn:xxx:de -NotBeforeSkew 3

ADFS Not Before Time Skew 

Enjoy!

Wednesday, January 18, 2017

WebAPI : Using Swagger and Postman with multiple POST

I've been busy on a WebAPI project where I need multiple POST actions in a a WebAPI controller.

There's a ton of stuff around best practice for this - some say each controller should only have one POST; others disagree.

I finally decided to break the controllers up by category and then within each controller have multiple POST functions.

I also added Swagger via Swashbuckle and used Postman for some unit tests so I thought I would write this all up.

As an example, I have a Maths controller with Add and Multiply.

The controller:

using MathsWebAPI.Models;
using System.Web.Http;

namespace MathsWebAPI.Controllers
{
    public class MathsController : ApiController
    {
        // POST: api/Maths
        [HttpPost]
        public int Add([FromBody] Maths info)
        {
            int number1 = info.number1;
            int number2 = info.number2;

            return (number1 + number2);
        }

        // POST: api/Maths
        [HttpPost]
        public int Multiply([FromBody] Maths info)
        {
            int number1 = info.number1;
            int number2 = info.number2;

            return (number1 * number2);
        }
    }
}

Note that I am passing in a model object as I have multiple parameters.

The model object:

namespace MathsWebAPI.Models
{
    public class Maths
    {
        public int number1 { get; set; }

        public int number2 { get; set; }
    }
}

The WebAPIConfig.cs:

using System.Web.Http;

namespace MathsWebAPI
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            /*config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );*/

            config.Routes.MapHttpRoute(
                name: "ControllerAndAction",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new {id = RouteParameter.Optional}
            );
        }
    }
}

In SwaggerConfig.cs, uncomment:

c.DisableValidator();

to avoid the error message.

Now if I navigate to:

http://localhost:44571/swagger

where 44571 is my IIS Express port

I see:


Now if you click the yellow area, it will populate the "Info" box with the model object format and you just have to fill in the values.

Click "Try it out" and you see:


 Note the URL is:

api/Maths/Add

Now if we add Postman, the commands are in this gist.

Running the script, we see:


Enjoy!

Friday, January 13, 2017

ASP : Classic ASP and ADFS

This question comes up from time to time and there was a recent one on the forum.

I did this back in the day so thought it would be a good idea to write up.

Classic ASP is basically just static HTML files, no web.config, no ASP pipeline, no code behind etc.

And they are still out there.

So we will integrate it with ADFS using WIF i.e. everything is driven by a web.config.

Which means we need to add one - example in this gist.

To test this, I used a Windows 8 PC.

This PC has both WIF 3.5 and WIF 4.5 installed. WIF 4.5 is built into ASP.NET 4.5.

So you may need to install this.

This is the line in the web.config:

section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"

Also WIF doesn't run on Server 2003 - hopefully you've upgraded by now :-)

The key to getting WIF to be added to the pipeline is this line:

modules runAllManagedModulesForAllRequests="true"

Note that the logging at the end is optional and this part of the file can be removed if not required.

All the above has to be done "on the fly". There is no way to create a Classic ASP project in Visual Studio. (In fact, Microsoft go out of their way to actively discourage this. The official line is that Classic ASP is no longer supported). Consequently, there is no context checking, intellisense or compile / run phase.

I didn't use VS at all - just Notepad++.

Also remember to enable ASP under Windows Features / IIS / World Wide Web Services / Application Development Features.

And you can't use an classic Application Pool in IIS - it needs to be integrated.

I just took a folder on my local PC and added a Default.asp (which contained some basic HTML) and the web.config.

Then I added this as an application in IIS. I called the application "ClassicASPBasic".

I then configured an RP on ADFS. There is no metadata so it easiest to do this manually.

Then configure the web.config. You just need to alter the "my-adfs" to your ADFS FQDN and "my-pc" to wherever you have the website hosted.

Also remember to update the thumbprint with your ADFS token-signing certificate.

Then in IIS on the RHS - "Browse Application" - use the port 443 link.

IIS sees the web.config. The web.config tells it to use the WIF classes. WIF by default protects everything so it sends you off to ADFS to authenticate. Authenticate and you are redirected back to the simple .asp page.

The next step is to Sign Out. You cannot use the standard WIF FederatedSignOut messages because there is no .NET framework so it has to be done via a URL link on the page. If you want the user to end up on the ADFS logout page, use:

&lt;a href="https://my-adfs/adfs/ls/?wa=wsignout1.0">Sign out&lt;/a>

If you want to the user to be redirected back to the application use:

&lt;a href="https://my-adfs/adfs/ls/?wa=wsignout1.0&wreply=https://xxx/yyy/Logout.asp">Sign out and return&lt;/a>

where:
  • xxx is the ADFS URL
  • yyy is the application URL
  • Logout.asp is a new page added to the site

The other way is to use VS and create an MVC application. Refer How To: Build Claims-Aware ASP.NET MVC Web Application Using WIF.

Then simply copy all your static files over.

This way allows you to access the claims as per the link. 

Enjoy!