keycloak-scim/securing_apps/topics/oidc/javascript-adapter.adoc
2022-03-25 12:08:02 -03:00

597 lines
28 KiB
Text

[[_javascript_adapter]]
=== JavaScript adapter
{project_name} comes with a client-side JavaScript library that can be used to secure HTML5/JavaScript applications. The JavaScript adapter has built-in support for Cordova applications.
The library can be retrieved directly from the {project_name} server at `{kc_js_path}/keycloak.js` and is also distributed as a ZIP archive.
A best practice is to load the JavaScript adapter directly from {project_name} Server as it will automatically be updated when you upgrade the server. If you copy the adapter to your web application instead, make sure you upgrade the adapter only after you have upgraded the server.
NOTE: You can also download package from npm: https://www.npmjs.com/package/keycloak-js
One important thing to note about using client-side applications is that the client has to be a public client as there is no secure way to store client
credentials in a client-side application. This makes it very important to make sure the redirect URIs you have configured for the client are correct and as specific as possible.
To use the JavaScript adapter you must first create a client for your application in the {project_name} Admin Console. Make sure `public`
is selected for `Access Type`.
You also need to configure `Valid Redirect URIs` and `Web Origins`. Be as specific as possible as failing to do so may result in a security vulnerability.
Once the client is created click on the `Installation` tab select `Keycloak OIDC JSON` for `Format Option` then click `Download`. The downloaded
`keycloak.json` file should be hosted on your web server at the same location as your HTML pages.
Alternatively, you can skip the configuration file and manually configure the adapter.
The following example shows how to initialize the JavaScript adapter:
[source,html]
----
<html>
<head>
<script src="keycloak.js"></script>
<script>
function initKeycloak() {
const keycloak = new Keycloak();
keycloak.init().then(function(authenticated) {
alert(authenticated ? 'authenticated' : 'not authenticated');
}).catch(function() {
alert('failed to initialize');
});
}
</script>
</head>
<body onload="initKeycloak()">
<!-- your page content goes here -->
</body>
</html>
----
If the `keycloak.json` file is in a different location you can specify it:
[source,javascript]
----
const keycloak = new Keycloak('http://localhost:8080/myapp/keycloak.json');
----
Alternatively, you can pass in a JavaScript object with the required configuration instead:
[source,javascript,subs="attributes+"]
----
const keycloak = new Keycloak({
url: 'http://keycloak-server${kc_base_path}',
realm: 'myrealm',
clientId: 'myapp'
});
----
By default to authenticate you need to call the `login` function. However, there are two options available to make the adapter automatically authenticate. You
can pass `login-required` or `check-sso` to the init function. `login-required` will authenticate the client if the user is logged-in to {project_name}
or display the login page if not. `check-sso` will only authenticate the client if the user is already logged-in, if the user is not logged-in the browser will be
redirected back to the application and remain unauthenticated.
You can configure a _silent_ `check-sso` option.
With this feature enabled, your browser won't do a full redirect to the {project_name} server and back to your application, but this action will be performed in a hidden iframe, so your application resources only need to be loaded and parsed once by the browser when the app is initialized and not again after the redirect back from {project_name} to your app.
This is particularly useful in case of SPAs (Single Page Applications).
To enable the _silent_ `check-sso`, you have to provide a `silentCheckSsoRedirectUri` attribute in the init method.
This URI needs to be a valid endpoint in the application (and of course it must be configured as a valid redirect for the client in the {project_name} Admin Console):
[source,javascript]
----
keycloak.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html'
})
----
The page at the silent check-sso redirect uri is loaded in the iframe after successfully checking your authentication state and retrieving the tokens from the {project_name} server.
It has no other task than sending the received tokens to the main application and should only look like this:
[source,html]
----
<html>
<body>
<script>
parent.postMessage(location.href, location.origin)
</script>
</body>
</html>
----
Please keep in mind that this page at the specified location must be provided by the application itself and is _not_ part of the JavaScript adapter!
WARNING: _Silent_ `check-sso` functionality is limited in some modern browsers. Please see the <<_modern_browsers,Modern Browsers with Tracking Protection Section>>.
To enable `login-required` set `onLoad` to `login-required` and pass to the init method:
[source,javascript]
----
keycloak.init({
onLoad: 'login-required'
})
----
After the user is authenticated the application can make requests to RESTful services secured by {project_name} by including the bearer token in the
`Authorization` header. For example:
[source,javascript]
----
const loadData = function () {
document.getElementById('username').innerText = keycloak.subject;
const url = 'http://localhost:8080/restful-service';
let req = new XMLHttpRequest();
req.open('GET', url, true);
req.setRequestHeader('Accept', 'application/json');
req.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);
req.onreadystatechange = function () {
if (req.readyState == 4) {
if (req.status == 200) {
alert('Success');
} else if (req.status == 403) {
alert('Forbidden');
}
}
}
req.send();
};
----
One thing to keep in mind is that the access token by default has a short life expiration so you may need to refresh the access token prior to sending the
request. You can do this by the `updateToken` method. The `updateToken` method returns a promise which makes it easy to invoke the service only if the
token was successfully refreshed and display an error to the user if it wasn't. For example:
[source,javascript]
----
keycloak.updateToken(30).then(function() {
loadData();
}).catch(function() {
alert('Failed to refresh token');
});
----
==== Session Status iframe
By default, the JavaScript adapter creates a hidden iframe that is used to detect if a Single-Sign Out has occurred.
This does not require any network traffic, instead the status is retrieved by looking at a special status cookie.
This feature can be disabled by setting `checkLoginIframe: false` in the options passed to the `init` method.
You should not rely on looking at this cookie directly. Its format can change and it's also associated with the URL of the {project_name} server, not
your application.
WARNING: Session Status iframe functionality is limited in some modern browsers. Please see <<_modern_browsers,Modern Browsers with Tracking Protection Section>>.
[[_javascript_implicit_flow]]
==== Implicit and hybrid flow
By default, the JavaScript adapter uses the https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[Authorization Code] flow.
With this flow the {project_name} server returns an authorization code, not an authentication token, to the application. The JavaScript adapter exchanges
the `code` for an access token and a refresh token after the browser is redirected back to the application.
{project_name} also supports the https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth[Implicit] flow where an access token
is sent immediately after successful authentication with {project_name}. This may have better performance than standard flow, as there is no additional
request to exchange the code for tokens, but it has implications when the access token expires.
However, sending the access token in the URL fragment can be a security vulnerability. For example the token could be leaked through web server logs and or
browser history.
To enable implicit flow, you need to enable the `Implicit Flow Enabled` flag for the client in the {project_name} Admin Console.
You also need to pass the parameter `flow` with value `implicit` to `init` method:
[source,javascript]
----
keycloak.init({
flow: 'implicit'
})
----
One thing to note is that only an access token is provided and there is no refresh token. This means that once the access token has expired the application
has to do the redirect to the {project_name} again to obtain a new access token.
{project_name} also supports the https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth[Hybrid] flow.
This requires the client to have both the `Standard Flow Enabled` and `Implicit Flow Enabled` flags enabled in the admin console.
The {project_name} server will then send both the code and tokens to your application.
The access token can be used immediately while the code can be exchanged for access and refresh tokens.
Similar to the implicit flow, the hybrid flow is good for performance because the access token is available immediately.
But, the token is still sent in the URL, and the security vulnerability mentioned earlier may still apply.
One advantage in the Hybrid flow is that the refresh token is made available to the application.
For the Hybrid flow, you need to pass the parameter `flow` with value `hybrid` to the `init` method:
[source,javascript]
----
keycloak.init({
flow: 'hybrid'
})
----
[#hybrid-apps-with-cordova]
==== Hybrid Apps with Cordova
Keycloak support hybrid mobile apps developed with https://cordova.apache.org/[Apache Cordova]. The JavaScript adapter has two modes for this: `cordova` and `cordova-native`:
The default is cordova, which the adapter will automatically select if no adapter type has been configured and window.cordova is present.
When logging in, it will open an https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-inappbrowser/[InApp Browser] that lets the user interact with {project_name} and afterwards returns to the app by redirecting to `http://localhost`. Because of this, you must whitelist this URL as a valid redirect-uri in the client configuration section of the Admin Console.
While this mode is easy to setup, it also has some disadvantages:
* The InApp-Browser is a browser embedded in the app and is not the phone's default browser. Therefore it will have different settings and stored credentials will not be available.
* The InApp-Browser might also be slower, especially when rendering more complex themes.
* There are security concerns to consider, before using this mode, such as that it is possible for the app to gain access to the credentials of the user, as it has full control of the browser rendering the login page, so do not allow its use in apps you do not trust.
Use this example app to help you get started: https://github.com/keycloak/keycloak/tree/master/examples/cordova
The alternative mode `cordova-native` takes a different approach.
It opens the login page using the system's browser.
After the user has authenticated, the browser redirects back into the app using a special URL.
From there, the {project_name} adapter can finish the login by reading the code or token from the URL.
You can activate the native mode by passing the adapter type `cordova-native` to the `init` method:
[source,javascript]
----
keycloak.init({
adapter: 'cordova-native'
})
----
This adapter required two additional plugins:
* https://github.com/google/cordova-plugin-browsertab[cordova-plugin-browsertab]: allows the app to open webpages in the system's browser
* https://github.com/e-imaxina/cordova-plugin-deeplinks[cordova-plugin-deeplinks]: allow the browser to redirect back to your app by special URLs
The technical details for linking to an app differ on each platform and special setup is needed.
Please refer to the Android and iOS sections of the https://github.com/e-imaxina/cordova-plugin-deeplinks/blob/master/README.md[deeplinks plugin documentation] for further instructions.
There are different kinds of links for opening apps: custom schemes (i.e. `myapp://login` or `android-app://com.example.myapp/https/example.com/login`) and https://developer.apple.com/ios/universal-links/[Universal Links (iOS)]) / https://developer.android.com/training/app-links/deep-linking[Deep Links (Android)].
While the former are easier to setup and tend to work more reliably, the later offer extra security as they are unique and only the owner of a domain can register them.
Custom-URLs are deprecated on iOS.
We recommend that you use universal links, combined with a fallback site with a custom-url link on it for best reliability.
Furthermore, we recommend the following steps to improve compatibility with the Keycloak Adapter:
* Universal Links on iOS seem to work more reliably with `response-mode` set to `query`
* To prevent Android from opening a new instance of your app on redirect add the following snippet to `config.xml`:
[source,xml]
----
<preference name="AndroidLaunchMode" value="singleTask" />
----
There is an example app that shows how to use the native-mode: https://github.com/keycloak/keycloak/tree/master/examples/cordova-native
[#custom-adapters]
==== Custom Adapters
Sometimes it's necessary to run the JavaScript client in environments that are not supported by default (such as Capacitor). To make it possible to use the JavasScript client in these kind of unknown environments is possible to pass a custom adapter. For example a 3rd party library could provide such an adapter to make it possible to run the JavaScript client without issues:
[source,javascript]
----
import Keycloak from 'keycloak-js';
import KeycloakCapacitorAdapter from 'keycloak-capacitor-adapter';
const keycloak = new Keycloak();
keycloak.init({
adapter: KeycloakCapacitorAdapter,
});
----
This specific package does not exist, but it gives a pretty good example of how such an adapter could be passed into the client.
It's also possible to make your own adapter, to do so you will have to implement the methods described in the `KeycloakAdapter` interface. For example the following TypeScript code ensures that all of the methods are properly implemented:
[source,typescript]
----
import Keycloak, { KeycloakAdapter } from 'keycloak-js';
// Implement the 'KeycloakAdapter' interface so that all required methods are guaranteed to be present.
const MyCustomAdapter: KeycloakAdapter = {
login(options) {
// Write your own implementation here.
}
// The other methods go here...
};
const keycloak = new Keycloak();
keycloak.init({
adapter: MyCustomAdapter,
});
----
Naturally you can also do this without TypeScript by omitting the type information, but ensuring implementing the interface properly will then be left entirely up to you.
==== Earlier Browsers
The JavaScript adapter depends on Base64 (window.btoa and window.atob), HTML5 History API and optionally the Promise API.
If you need to support browsers that do not have these available (for example, IE9) you need to add polyfillers.
Example polyfill libraries:
* Base64 - https://github.com/davidchambers/Base64.js
* HTML5 History - https://github.com/devote/HTML5-History-API
* Promise - https://github.com/stefanpenner/es6-promise
[[_modern_browsers]]
==== Modern Browsers with Tracking Protection
In the latest versions of some browsers various cookies policies are applied to prevent tracking of the users by third-parties,
like SameSite in Chrome or completely blocked third-party cookies. It is expected that those policies will become even
more restrictive and adopted by other browsers over time, eventually leading to cookies in third-party contexts to be
completely unsupported and blocked by the browsers. The adapter features affected by this might get deprecated in the
future.
Javascript adapter relies on third-party cookies for Session Status iframe, _silent_ `check-sso` and partially also for
regular (non-silent) `check-sso`. Those features have limited functionality or are completely disabled based on how
the browser is restrictive regarding cookies. The adapter tries to detect this setting and reacts accordingly.
===== Browsers with "SameSite=Lax by Default" Policy
All features are supported if SSL / TLS connection is configured on the {project_name} side as well as on the application
side. See link:{installguide_link}#_setting_up_ssl[configuring the SSL / TLS]. Affected is for example Chrome starting with
version 84.
===== Browsers with Blocked Third-Party Cookies
Session Status iframe is not supported and is automatically disabled if such browser behavior is detected by the JS adapter.
This means the adapter cannot use session cookie for Single Sign-Out detection and have to rely purely on tokens. This
implies that when user logs out in another window, the application using JavaScript adapter won't be logged out until it
tries to refresh the Access Token. Therefore, it is recommended to set Access Token Lifespan to relatively short time, so
that the logout is detected rather sooner than later. Please see link:{adminguide_link}#_timeouts[Session and Token Timeouts].
_Silent_ `check-sso` is not supported and falls back to regular (non-silent) `check-sso` by default. This behaviour can
be changed by setting `silentCheckSsoFallback: false` in the options passed to the `init` method. In this case, `check-sso`
will be completely disabled if restrictive browser behavior is detected.
Regular `check-sso` is affected as well. Since Session Status iframe is unsupported, an additional redirect to {project_name}
has to be made when the adapter is initialized to check user's login status. This is different from standard behavior when
the iframe is used to tell whether the user is logged in, and the redirect is performed only when logged out.
An affected browser is for example Safari starting with version 13.1.
==== JavaScript Adapter Reference
===== Constructor
[source,javascript,subs="attributes+"]
----
new Keycloak();
new Keycloak('http://localhost/keycloak.json');
new Keycloak({ url: 'http://localhost{kc_base_path}', realm: 'myrealm', clientId: 'myApp' });
----
===== Properties
authenticated::
Is `true` if the user is authenticated, `false` otherwise.
token::
The base64 encoded token that can be sent in the `Authorization` header in requests to services.
tokenParsed::
The parsed token as a JavaScript object.
subject::
The user id.
idToken::
The base64 encoded ID token.
idTokenParsed::
The parsed id token as a JavaScript object.
realmAccess::
The realm roles associated with the token.
resourceAccess::
The resource roles associated with the token.
refreshToken::
The base64 encoded refresh token that can be used to retrieve a new token.
refreshTokenParsed::
The parsed refresh token as a JavaScript object.
timeSkew::
The estimated time difference between the browser time and the {project_name} server in seconds. This value is just an estimation, but is accurate
enough when determining if a token is expired or not.
responseMode::
Response mode passed in init (default value is fragment).
flow::
Flow passed in init.
adapter::
Allows you to override the way that redirects and other browser-related functions will be handled by the library.
Available options:
* "default" - the library uses the browser api for redirects (this is the default)
* "cordova" - the library will try to use the InAppBrowser cordova plugin to load keycloak login/registration pages (this is used automatically when the library is working in a cordova ecosystem)
* "cordova-native" - the library tries to open the login and registration page using the phone's system browser using the BrowserTabs cordova plugin. This requires extra setup for redirecting back to the app (see <<hybrid-apps-with-cordova>>).
* custom - allows you to implement a custom adapter (only for advanced use cases)
responseType::
Response type sent to {project_name} with login requests. This is determined based on the flow value used during initialization, but can be overridden by setting this value.
===== Methods
====== init(options)
Called to initialize the adapter.
Options is an Object, where:
* useNonce - Adds a cryptographic nonce to verify that the authentication response matches the request (default is `true`).
* onLoad - Specifies an action to do on load. Supported values are `login-required` or `check-sso`.
* silentCheckSsoRedirectUri - Set the redirect uri for silent authentication check if onLoad is set to 'check-sso'.
* silentCheckSsoFallback - Enables fall back to regular `check-sso` when _silent_ `check-sso` is not supported by the browser (default is `true`).
* token - Set an initial value for the token.
* refreshToken - Set an initial value for the refresh token.
* idToken - Set an initial value for the id token (only together with token or refreshToken).
* timeSkew - Set an initial value for skew between local time and {project_name} server in seconds (only together with token or refreshToken).
* checkLoginIframe - Set to enable/disable monitoring login state (default is `true`).
* checkLoginIframeInterval - Set the interval to check login state (default is 5 seconds).
* responseMode - Set the OpenID Connect response mode send to {project_name} server at login request. Valid values are `query` or `fragment`. Default value is `fragment`, which means that after successful authentication will {project_name} redirect to JavaScript application with OpenID Connect parameters added in URL fragment. This is generally safer and recommended over `query`.
* flow - Set the OpenID Connect flow. Valid values are `standard`, `implicit` or `hybrid`.
* enableLogging - Enables logging messages from Keycloak to the console (default is `false`).
* pkceMethod - The method for Proof Key Code Exchange (https://datatracker.ietf.org/doc/html/rfc7636[PKCE]) to use. Configuring this value enables the PKCE mechanism. Available options:
- "S256" - The SHA256 based PKCE method
* messageReceiveTimeout - Set a timeout in milliseconds for waiting for message responses from the Keycloak server. This is used, for example, when waiting for a message during 3rd party cookies check. The default value is 10000.
Returns a promise that resolves when initialization completes.
====== login(options)
Redirects to login form.
Options is an optional Object, where:
* redirectUri - Specifies the uri to redirect to after login.
* prompt - This parameter allows to slightly customize the login flow on the {project_name} server side.
For example enforce displaying the login screen in case of value `login`. See link:{adapterguide_link}#_params_forwarding[Parameters Forwarding Section]
for the details and all the possible values of the `prompt` parameter.
* maxAge - Used just if user is already authenticated. Specifies maximum time since the authentication of user happened. If user is already authenticated for longer time than `maxAge`, the SSO is ignored and he will need to re-authenticate again.
* loginHint - Used to pre-fill the username/email field on the login form.
* scope - Used to forward the scope parameter to the {project_name} login endpoint. Use a space-delimited list of scopes. Those typically
reference link:{adminguide_link}#_client_scopes[Client scopes] defined on particular client. Note that the scope `openid` will be
always be added to the list of scopes by the adapter. For example, if you enter the scope options `address phone`, then the request
to {project_name} will contain the scope parameter `scope=openid address phone`.
* idpHint - Used to tell {project_name} to skip showing the login page and automatically redirect to the specified identity
provider instead. More info in the link:{adminguide_link}#_client_suggested_idp[Identity Provider documentation].
* acr - Contains the information about `acr` claim, which will be sent inside `claims` parameter to the {project_name} server. Typical usage
is for step-up authentication. Example of use `{ values: ["silver", "gold"], essential: true }`. See OpenID Connect specification
and link:{adminguide_link}#_step-up-flow[Step-up authentication documentation] for more details.
* action - If value is `register` then user is redirected to registration page, if the value is `UPDATE_PASSWORD` then the user will redirected to the reset password page (if not authenticated will send user to login page first and redirect after authenticated), otherwise to login page.
* locale - Sets the 'ui_locales' query param in compliance with https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest[section 3.1.2.1 of the OIDC 1.0 specification].
* cordovaOptions - Specifies the arguments that are passed to the Cordova in-app-browser (if applicable). Options `hidden` and `location` are not affected by these arguments. All available options are defined at https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-inappbrowser/. Example of use: `{ zoom: "no", hardwareback: "yes" }`;
====== createLoginUrl(options)
Returns the URL to login form.
Options is an optional Object, which supports same options as the function `login` .
====== logout(options)
Redirects to logout.
Options is an Object, where:
* redirectUri - Specifies the uri to redirect to after logout.
====== createLogoutUrl(options)
Returns the URL to logout the user.
Options is an Object, where:
* redirectUri - Specifies the uri to redirect to after logout.
====== register(options)
Redirects to registration form. Shortcut for login with option action = 'register'
Options are same as for the login method but 'action' is set to 'register'
====== createRegisterUrl(options)
Returns the url to registration page. Shortcut for createLoginUrl with option action = 'register'
Options are same as for the createLoginUrl method but 'action' is set to 'register'
====== accountManagement()
Redirects to the Account Management Console.
====== createAccountUrl(options)
Returns the URL to the Account Management Console.
Options is an Object, where:
* redirectUri - Specifies the uri to redirect to when redirecting back to the application.
====== hasRealmRole(role)
Returns true if the token has the given realm role.
====== hasResourceRole(role, resource)
Returns true if the token has the given role for the resource (resource is optional, if not specified clientId is used).
====== loadUserProfile()
Loads the users profile.
Returns a promise that resolves with the profile.
For example:
[source,javascript]
----
keycloak.loadUserProfile()
.then(function(profile) {
alert(JSON.stringify(profile, null, " "))
}).catch(function() {
alert('Failed to load user profile');
});
----
====== isTokenExpired(minValidity)
Returns true if the token has less than minValidity seconds left before it expires (minValidity is optional, if not specified 0 is used).
====== updateToken(minValidity)
If the token expires within minValidity seconds (minValidity is optional, if not specified 5 is used) the token is refreshed.
If the session status iframe is enabled, the session status is also checked.
Returns a promise that resolves with a boolean indicating whether or not the token has been refreshed.
For example:
[source,javascript]
----
keycloak.updateToken(5)
.then(function(refreshed) {
if (refreshed) {
alert('Token was successfully refreshed');
} else {
alert('Token is still valid');
}
}).catch(function() {
alert('Failed to refresh the token, or the session has expired');
});
----
====== clearToken()
Clear authentication state, including tokens.
This can be useful if application has detected the session was expired, for example if updating token fails.
Invoking this results in onAuthLogout callback listener being invoked.
===== Callback Events
The adapter supports setting callback listeners for certain events.
For example:
[source,javascript]
----
keycloak.onAuthSuccess = function() { alert('authenticated'); }
----
The available events are:
* onReady(authenticated) - Called when the adapter is initialized.
* onAuthSuccess - Called when a user is successfully authenticated.
* onAuthError - Called if there was an error during authentication.
* onAuthRefreshSuccess - Called when the token is refreshed.
* onAuthRefreshError - Called if there was an error while trying to refresh the token.
* onAuthLogout - Called if the user is logged out (will only be called if the session status iframe is enabled, or in Cordova mode).
* onTokenExpired - Called when the access token is expired. If a refresh token is available the token can be refreshed with updateToken, or in cases where it is not (that is, with implicit flow) you can redirect to login screen to obtain a new access token.