94 lines
5.4 KiB
Text
94 lines
5.4 KiB
Text
|
|
=== Client Initiated Account Linking
|
|
|
|
Some applications want to integrate with social providers like Facebook, but do not want to provide an option to login via
|
|
these social providers. {{book.project.name}} offers a browser-based API that applications can use to link an existing
|
|
user account to a specific external IDP. This is called client initiated account linking.
|
|
|
|
The way it works is that the application forward's the user's browser to a URL on the {{book.project.name}} server requesting
|
|
that it wants to link the user's account to a specific external provider (i.e. Facebook). The server
|
|
initiates a login with the external provider. The browser logs in at the external provider and is redirected
|
|
back to the auth server. The auth server establishes the link and redirects back to the application with a confirmation.
|
|
|
|
There are some preconditions that must be met by the client application before it can initiate this protocol:
|
|
|
|
* The desired identity provider must be configured and enabled for the user's realm in the admin console.
|
|
* The application must already be logged in as an existing user via the OIDC protocol
|
|
* The user must have an `account.manage-account` or `account.manage-account-links` role mapping.
|
|
* The application must be granted the scope for those roles within its access token
|
|
* The application must have access to its access token as it needs information within it to generate the redirect URL.
|
|
|
|
To initiate the login, the application must fabricate a URL and redirect the user's browser to this URL. The URL looks like this:
|
|
|
|
[source,java]
|
|
----
|
|
{auth-server-root}/auth/realms/{realm}/broker/{provider}/linking?client_id={id}&redirect_uri={uri}&nonce={nonce}&hash={hash}
|
|
----
|
|
|
|
Here's a description of each path and query param:
|
|
|
|
provider::
|
|
This is the provider alias of the external IDP that you defined in the `Identity Provider` section of the admin console.
|
|
|
|
client_id::
|
|
This is the OIDC client id of your application. When you registered the application as a client in the admin console,
|
|
you had to specify this client id.
|
|
|
|
redirect_uri::
|
|
This is the application callback URL you want to redirect to after the account link is established. It must be a valid
|
|
client redirect URI pattern. In other words, it must match one of the valid URL patterns you defined when you registered
|
|
the client in the admin console.
|
|
|
|
nonce::
|
|
This is a random string that your application must generate
|
|
|
|
hash::
|
|
This is a Base64 URL encoded hash. This hash is generated by Base64 URL encoding a SHA_256 hash of `nonce` + `token.getSessionState()` + `token.getClientSession()` + `provider`
|
|
The token variable are obtained from the OIDC access token. Basically you are hashing the random nonce, the user session id, the client session id, and the identity
|
|
provider alias you want to access.
|
|
|
|
Here's an example of Java Servlet code that generates the URL to establish the account link.
|
|
|
|
|
|
[source,java]
|
|
----
|
|
KeycloakSecurityContext session = (KeycloakSecurityContext) httpServletRequest.getAttribute(KeycloakSecurityContext.class.getName());
|
|
AccessToken token = session.getToken();
|
|
String clientSessionId = token.getClientSession();
|
|
String nonce = UUID.randomUUID().toString();
|
|
MessageDigest md = null;
|
|
try {
|
|
md = MessageDigest.getInstance("SHA-256");
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
String input = nonce + token.getSessionState() + clientSessionId + provider;
|
|
byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
|
String hash = Base64Url.encode(check);
|
|
request.getSession().setAttribute("hash", hash);
|
|
String redirectUri = ...;
|
|
String accountLinkUrl = KeycloakUriBuilder.fromUri(authServerRootUrl)
|
|
.path("/auth/realms/{realm}/broker/{provider}/link")
|
|
.queryParam("nonce", nonce)
|
|
.queryParam("hash", hash)
|
|
.queryParam("client_id", token.getIssuedFor())
|
|
.queryParam("redirect_uri", redirectUri).build(realm, provider).toString();
|
|
----
|
|
|
|
Why is this hash included? We do this so that the auth server is guaranteed to know that the client application initiated the request and no other rogue app
|
|
just randomly asked for a user account to be linked to a specific provider. The auth server will first check to see if the user is logged in by checking the SSO
|
|
cookie set at login. It will then try to regenerate the hash based on the current login and match it up to the hash sent by the application.
|
|
|
|
After the account has been linked, the auth server will redirect back to the `redirect_uri`. If there is a problem servicing the link request,
|
|
the auth server may or may not redirect back to the `redirect_uri`. The browser may just end up at an error page instead of being redirected back
|
|
to the application. If there is an error condition and the auth server deems it safe enough to redirect back to the client app, an additional
|
|
`error` query parameter will be appended to the `redirect_uri`.
|
|
|
|
[WARNING]
|
|
While this API guarantees that the application initiated the request, it does not completely prevent CSRF attacks for this operation. The application
|
|
is still responsible for guarding against CSRF attacks target at itself.
|
|
|
|
==== Refreshing External Tokens
|
|
|
|
If you are using the external token generated by logging into the provider (i.e. a Facebook or Github token), you can refresh this token by re-initiating the account linking API.
|
|
|