Monday, May 15, 2017

ADFS : You can change anything in the Theme structure

The official document around onload.js and customising the ADFS login page for 2012 R2 and 2016 (ADFS 3.0 and 4.0) is here.

The Theme directory structure looks like:

Directory of C:\...\Theme\Custom
05/11/2017 09:26 PM .
05/11/2017 09:26 PM ..
05/11/2017 09:26 PM css
05/11/2017 09:26 PM illustration
05/11/2017 09:26 PM images
05/11/2017 09:26 PM script
0 File(s) 0 bytes


Directory of C:\...\Theme\Custom\css
05/11/2017 09:26 PM .
05/11/2017 09:26 PM ..
05/11/2017 09:26 PM 8,144 style.css
05/11/2017 09:26 PM 8,146 style.rtl.css
2 File(s) 16,290 bytes

Directory of C:\...\Theme\Custom\illustration
05/11/2017 09:26 PM .
05/11/2017 09:26 PM ..
05/11/2017 09:26 PM 116,699 illustration.png
1 File(s) 116,699 bytes

Directory of C:\...\Theme\Custom\images
05/11/2017 09:26 PM .
05/11/2017 09:26 PM ..
05/14/2017 08:45 PM idp
0 File(s) 0 bytes

Directory of C:\...\Theme\Custom\images\idp
05/14/2017 08:45 PM .
05/14/2017 08:45 PM ..
05/11/2017 09:26 PM 931 idp.png
05/11/2017 09:32 PM 1,727 localsts.png
05/11/2017 09:26 PM 1,977 otherorganizations.png
3 File(s) 4,635 bytes

Directory of C:\...\Theme\Custom\script
05/11/2017 09:26 PM .
05/11/2017 09:26 PM ..
05/11/2017 10:23 PM 5,925 onload.js
1 File(s) 5,925 bytes 


The document refers to onload.js but you can use the same commands for any of the above files.

@Pierre e.g. has blogged on changing the "thumbnails" here.

The command for this then looks like:

Set-AdfsWebTheme -TargetName MyTheme -AdditionalFileResource @{Uri="/adfs/portal/images/idp/idp.png";path="C:\MyTheme\images\idp\idp-adfs.png"} 

Enjoy!

Friday, May 12, 2017

ADFS : Avoiding the Home Realm Discovery screen by using a link

I was asked if given the HRD screen is just "buttons" and there was a "link" behind that and users always picked the same one. could they not simply add the "link" to their web site and avoid the HRD screen altogether?

The basic problem with this is that the link is not static – it’s actually dynamic.

Take SAML-P as a example.

Assume the HRD screen has two entries; ADFS A and ADFS B.

The SAML request from the application to ADFS A gives:

https://adfs-a/adfs/ls/?SAMLRequest=

And then selecting ADFS B via HRD gives:

https://adfs-b/adfs/ls/?SAMLRequest=...

The SAML request itself is different because as the packet goes through each ADFS, it’s signed with that ADFS’s signing key.

So the "link" would have to be updated every time a signing certificate changed.

In addition, a section of the SAML AuthnRequest contains:

IssueInstant="2017-05-11T01:27:25.56Z"

The request contains the time the request was sent – which is dynamic.

There is an ADFS parameter for SAML called “clock skew”.

ADFS rejects the request when the skew between the server clock and the time in the request is more than 30 seconds (by default).

So nice try but no cigar!

Enjoy!

ADFS : Augmenting the default JWT with additional attributes

This is for Server 2016 - ADFS 4.0.

The standard use case is for an ASP.NET application using OpenID Connect / OAuth via the NuGet OWIN packages taking to ADFS.


The standard way is to configure a server application as above.

This all works . The problem is that the ADFS wizard does not have any way to configure claims rules. As a result you get the standard set of claims in the JWT.

aud     8173...1501
iss     https://xxx.cloudapp.net/adfs
iat     1494378378
exp     1494381978
http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant     1494378377
nonce     6362....NmVl
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier     0JrA...H7k=
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn     user1@dev.local
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name     DEV\user1
c_hash     baOc...R8GA


The only user specific details are the "upn" and the "name".

But what if you want more?

You can use the "Web browser accessing a web application" profile as per this.

This profile achieves the result in a roundabout way:

"Behind the scenes, this template creates a native client and new app type called Web application, which is just a Web API with an Identifier (RPID) that matches the native client's client ID. This means the Web application is simultaneously client and resource, so you can assign issuance transform rules as you would with a Web API."

I configured some claims in the wizard:


Now when I authenticate, I get this:

aud     c906...3ab4
iss     https://xxx.cloudapp.net/adfs
iat     1494379409
exp     1494383009
http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant     1494379408
nonce     6362...NTk2
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier     PRUA...50U=
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name     DEV\user1
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn     user1@dev.local
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname     User1
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname     Test
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress     user1@company.com
http://schemas.xmlsoap.org/claims/CommonName     User1 Test
apptype     Public
appid     c906...3ab4
http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod     urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
ver     1.0
http://schemas.microsoft.com/identity/claims/scope     openid
c_hash     -0ZX...Z81A


You will also notice that the apptype is "Public" i.e. a web site as opposed to "confidential" i.e. a web API.

I also answered a question in stackoverflow along these lines here.

As per the discussion from @Efrain:

"As indicated in @nzpcmad's answer, it appears that custom claims in the id_token using the default URL-parameter-encoded GET redirect is simply not supported. The reason for this may be that there is an URL length limit, but I find that quite questionable.

Anyway, apparently this restriction does not apply when the token is returned in a POST redirect. That's also why people describe it working just fine for MVC applications.

So I was able to work around the problem by redirecting the response to a back-end API endpoint (POST), which just redirects it to the front-end (SPA) again, but as a GET request with URL-encoded parameters:

public class LoginController : ApiController
{
    [HttpPost]
    [Route("login")]
    public HttpResponseMessage Login(FormDataCollection formData)
    {
        var token = formData["id_token"];
        var state = formData["state"];
        var response = Request.CreateResponse(HttpStatusCode.Moved);
        var frontendUri = ConfigurationManager.AppSettings["ad:FrontendUri"];
        response.Headers.Location = new Uri($"{frontendUri}#id_token={token}
                                    &state={state}");
        return response;
    }
}
Note that to change the response method from GET to POST, one simply has to add &response_mode=form_post to the OAuth request URL."

Enjoy!

Monday, May 08, 2017

ADFS : Passing NameID across CP and RP

Imagine the following:

CP A --> federated with CP B --> RP

So a user goes to the RP and via HRD on CP B selects CP A and authenticates against AD.

The claims derived from CP A need to be passed across.

On CP A we have the standard LDAP rules since the user authenticated against that AD.

One of the claims we want to pass across is NameID.

You have to configure pass-through rules on CP B and the RP.

So the claims are configured in three places.

The problem is that NameID never makes it across.

There are a number of posts from people reporting the same thing but no solution.

The way I got around it was:

Assume that we want sAMAccountName to be NameID.

On CP A, have a LDAP rule:

sAMAccountName --> http://company/claims/sAMAccountName

Plus pass-through all the other claims.

On CP B, have a Transform rule:

http://company/claims/sAMAccountName --> NameID

Plus pass-through all the other claims. 

In the RP, pass-through all the claims including NameID.

Now imagine you have two RP, RP A and RP B.

RP A wants sAMAccountName to be NameID.

RP B wants UPN to be NameID.

Now we have a problem because they both share the same pipeline CP A and CP B. You can't have two different rules both passing NameID.

What you have to do on CP A is:

UPN --> http://company/claims/UPN

Here you pass both http://company/claims/sAMAccountName and http://company/claims/UPN through on CP B and then transform them at the RP level to the NameID; one for each.

Enjoy!