I've only been able to figure out how to get an id token using B2C - but then I lose all the benefits of regular AAD apps (specifically access tokens, scopes and user consent)
Below I'll describe a simplified scenario, and what I've tried.
Scenario
Imagine I am developing a client (javascript SPA) and two services (WebAPI):
- A is a WebAPI-based service with two scopes (ReadA and WriteA), registered and hosted in Azure
- B is another WebAPI-based service with two scopes (ReadB and WriteB), registered and hosted in Azure
- C is a Javascript SPA
Now I want the user to sign in using Azure B2C, in a way which yields my client C the following tokens:
- an id token, so the client C can address me by name
- an access token for service A, with scopes ReadA and WriteA (issued after the usual user consent)
- an access token for service B, with scopes ReadB (issued after the usual user consent)
Can this be done? And how? Any examples out there?
All the examples I've been able to find shows one client authenticating the user, and a few show how to get a single access token (no scope support)
What I've tried sofar
My experiments sofar have been carried out using
- My own B2C tenant registered using recipe Azure Active Directory B2C: Create an Azure AD B2C tenant (with just local accounts for initial simplicity) - let's call it
fooplanner.onmicrosoft.com
- Service A registered and deployed in that tenant using recipe Create an API app in Azure and deploy code to it (with scopes ReadA and WriteA defined) - with application ID
AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA
- Service B registered and deployed in that tenant using recipe Create an API app in Azure and deploy code to it (with scopes ReadB defined) - with application ID
BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB
- The application FooPlanner registered with B2C - with ID
FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF
- Client C uses
oidc-client.js
- I would have used adal.js, but it doesn't suppport B2C (and besides, it's apparently being superceded by MSA, which doesn't even support Javascript yet...)
To avoid confusion relating to client-side libraries, my experiments below will be described in terms of the requests and responses sent and received, as reported by Fiddler.
Single client - ID token only
This is the baseline scenario, shown by most of the how-tos I've found.
Client C sends the following request to B2C:
https://login.microsoftonline.com/fooplanner.onmicrosoft.com/oauth2/v2.0/authorize?
p=b2c_1_fooplanner-signuporsignin&
client_id=ffffffff-ffff-ffff-ffff-ffffffffffff&
response_type=id_token&
scope=openid email profile
After prompting me for my credentials, B2C then eventually returns a single id_token
(where uuu...
is the GUID for my user entry in B2C):
id-token:
{
"ver": "1.0",
"iss": "https://login.microsoftonline.com/08de3e5f-6a10-4d7c-a0e3-fc4a627a712b/v2.0/",
"sub": "uuuuuuuu-uuuu-uuuu-uuuu-uuuuuuuuuuuu",
"aud": "ffffffff-ffff-ffff-ffff-ffffffffffff",
"oid": "uuuuuuuu-uuuu-uuuu-uuuu-uuuuuuuuuuuu",
"name": "Thomas"
"tfp": "B2C_1_fooplanner-signuporsignin"
}
(for brevity, I've omitted all the OAuth2 redirects, the Base64 JWT decoding etc. - I've even left out timestamps, nonces etc from the tokens. If they're relevant, I can supply full details)
This is received and handled as expected by oidc-client.js: I end up with an ID token, and no access token.
Single client - ID token + access token
After a little digging, I found a way to get an access token too: include the B2C application ID in the scopes, and ask for both token
and id_token
response types.
In this variant, client C sends the following request to B2C:
https://login.microsoftonline.com/fooplanner.onmicrosoft.com/oauth2/v2.0/authorize?
p=b2c_1_fooplanner-signuporsignin&
client_id=ffffffff-ffff-ffff-ffff-ffffffffffff&
response_type=token id_token&
scope=openid email profile ffffffff-ffff-ffff-ffff-ffffffffffff
B2C then eventually returns an id_token
and an access_token
:
id_token:
{
"ver": "1.0",
"iss": "https://login.microsoftonline.com/08de3e5f-6a10-4d7c-a0e3-fc4a627a712b/v2.0/",
"sub": "uuuuuuuu-uuuu-uuuu-uuuu-uuuuuuuuuuuu",
"aud": "ffffffff-ffff-ffff-ffff-ffffffffffff",
"oid": "uuuuuuuu-uuuu-uuuu-uuuu-uuuuuuuuuuuu",
"name": "Thomas"
"tfp": "B2C_1_fooplanner-signuporsignin"
}
access_token:
{
"iss": "https://login.microsoftonline.com/08de3e5f-6a10-4d7c-a0e3-fc4a627a712b/v2.0/",
"aud": "ffffffff-ffff-ffff-ffff-ffffffffffff",
"oid": "uuuuuuuu-uuuu-uuuu-uuuu-uuuuuuuuuuuu",
"sub": "uuuuuuuu-uuuu-uuuu-uuuu-uuuuuuuuuuuu",
"name": "Thomas",
"tfp": "B2C_1_fooplanner-signuporsignin",
}
This is again received and handled as expected by oidc-client.js: I end up with an ID token and an access token.
Notice however how suspiciously familiar the two tokens are - but then again, I'm asking for an access token for a B2C application, not a properly registered (AAD) application.
Single client, single service
So I thought: let's follow the previous approach - only this time, ask for an access token for one of the two real services.
Request:
https://login.microsoftonline.com/fooplanner.onmicrosoft.com/oauth2/v2.0/authorize?
p=b2c_1_fooplanner-signuporsignin&
client_id=ffffffff-ffff-ffff-ffff-ffffffffffff&
response_type=token id_token&
scope=openid email profile aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
This however complains about an unknown scope (aaa...
) - so either I'm misusing the protocol, or B2C doesn't know about regular AAD apps (in the same tenant, mind you).
Speculation: use the authorization endpoint?
I've read somewhere (the OpenID spec?) that an IdP (i.e. B2C) has an authorization endpoint that you can use to exchange an id_token
for an access_token
.
Would this be the way to approach this? And are there any client-side libraries out there supporting this?