OAuth Reference¶
This page discusses support for CiviCRM as an OAuth2 client. This was introduced in CiviCRM v5.32, but it is hidden by default.
It will be more visible in future versions.
The OAuth protocols define an access-control system for Internet-based services. Compared to traditional access-control mechanisms (like simple password-based logins), OAuth provides more security features. For example, it can grant limited access to specific resources, and it can de-list devices or applications which misbehave. These features make it well-suited to large-scale technology providers like Google, Microsoft, or Facebook. If you wish to have CiviCRM interact with their APIs, then you probably need to configure OAuth.
Before beginning development with CiviCRM and OAuth, you may wish to read:
- CiviCRM System Administrator Guide: Setup: OAuth
- Digital Ocean: An Introduction to OAuth 2
- RFC 6749: The OAuth 2.0 Authorization Framework
At time of writing, most implementations use OAuth version 2 (OAuth2). For this developer documentation, we will consider how to use CiviCRM as an OAuth2 client which accesses resources from a remote provider.
Model¶
The core extension oauth-client
defines a programmatic model and helpers for managing CiviCRM as an OAuth2 client.
Specifically, it exposes APIv4 entities (OAuthProvider
, OAuthClient
, OAuthSysToken
, OAuthSessionToken
and OAuthContactToken
). We'll
look at the entity definitions generally before we dive into usage examples.
Provider¶
Every remote service provider (such as Google or Microsoft) should be listed as a provider (OAuthProvider
). This record gives
well-known details about the provider, such as its name and authorization URL.
CiviCRM's OAuthProvider
is built on top of the PHP League's oauth2-client.
Key properties of the OAuthProvider
include:
Property | Type | Description | Default |
---|---|---|---|
name |
string |
Symbolic name such as gmail or ms-exchange |
n/a |
title |
string |
Displayable title such as "Google Mail" or "Microsoft Exchange Online" | n/a |
class |
string |
PHP class. In many cases, this is a generic class (e.g. League\OAuth2\Client\Provider\GenericProvider or Civi\OAuth\CiviGenericProvider ). In some cases, it may be a specialized class (e.g. League\OAuth2\Client\Provider\Google ). |
Civi\OAuth\CiviGenericProvider |
options |
array |
Open-ended list of arguments to give to the provider class. Below are some common sub-properties supported by generic classes: | |
options . urlAuthorize |
string |
Well-known end-point for initiating authorization workflows | n/a |
options . urlAccessToken |
string |
Well-known end-point for requesting access-tokens | n/a |
options . urlResourceOwnerDetails |
string |
Well-known end-point for getting expanded information about the user. May be null . For CiviGenericProvider , this may use the string-literal {{use_id_token}} for OpenID Connect. |
n/a |
options . scopeSeparator |
string |
||
options . scopes |
array |
List of scopes to request access to | n/a |
options . tenancy |
bool |
true if the provider URLs require a tenant ID to be passed in, using the {{tenant}} token |
n/a |
mailSettingsTemplate |
array |
If the provider is specifically used for registering email accounts (MailSettings ), then you can define a template for the MailSettings . |
|
contactTemplate |
array |
If the provider is used for creating contacts, you must define a template for new contacts. |
Note: OAuthProvider
is a logical entity managed via hook. It is not stored in the MySQL database.
Client¶
To send requests to a provider, each CiviCRM deployment must be pre-registered with the provider. As part of this registration,
the provider assigns a "Client ID" and "Client Secret". These are stored as properties in an OAuthClient
record:
Property | Type | Description | Default |
---|---|---|---|
provider |
string |
Symbolic name of the provider, such as gmail or ms-exchange |
n/a |
guid |
string |
The unique, public "Client ID" assigned by the remote provider. This is typically a long alphanumeric identifier. | n/a |
tenant |
string |
(Optional) The tenancy or organisation ID assigned by the remote provider to dedicated services | NULL |
secret |
string |
The secret "Client Secret" assigned by the remote provider. | n/a |
id |
int |
The unique, internal "ID" assigned by MySQL. This is typically a short number. | auto |
Question: What is the difference between client id
and guid
?
In OAuth2's specification, the client "ID" refers to a long, public identifier (often random alphanumerics) registered with the remote
web-service. In CiviCRM's data management system, an "ID" is an internal, incrementing integer. Each OAuthClient
will have both
identifiers, stored in two fields:
- The
id
field is the local/internal identifier required by the data-management layer. It is an incrementing integer. - The
guid
field is the public/external identifier. It is the value presented in web-service requests.
Informally, the word "ID" may describe either. In technical examples referencing the Civi API, the symbols id
and
guid
should be interpreted as id
(local/internal) and guid
(public/external).
Token¶
When a user or administrator authorizes CiviCRM to access resources, the OAuth protocol will ultimately produce an access token.
The lifecycle of an access-token depends on the use-case — based on the preferred lifecycle, one may choose different
storage options. The oauth-client
provides three forms of storage:
- System token (
OAuthSysToken
): Some access-tokens are used by an automated agent of the system when executing background jobs. (Example: Periodically polling a system mailbox for email-bounces.) These are system tokens. These tokens are stored in the database as first-order records, and they are not strictly attached to one CiviCRM user. - Contact token (
OAuthContactToken
): Some access-tokens are associated with a particular CiviCRM contact, and need to be stored so they can be used at a later time, perhaps repeatedly. (Example: Periodically updating a contact's photo by fetching it from their account on another site.) These tokens are stored in the database as first-order records. - Session token (
OAuthSessionToken
): Some access-tokens are only needed for a brief period — while the user interacts with the application. (Example: Importing a spreadsheet from Google Docs.) Each token is strictly associated with one logged-in user's interaction with CiviCRM (see notes below regarding anonymous users). After the user's momentary interaction, the token can (and probably should) be forgotten.
Key properties of a token record:
Property | Type | Description |
---|---|---|
client_id |
int |
Reference to the OAuthClient which obtained the token. |
grant_type |
string |
The procedure used to request this token. Ex: authorization_code , client_credentials , password |
scopes |
array |
List of scopes that were requested for this token. Some providers may emit a confirmation about the approved scopes — in which case this is updated. |
token_type |
string |
Indicates how the token may be used for sending requests. Typically, Bearer . |
access_token |
string |
A long, opaque value used for accessing resources (via HTTP/REST/IMAP/etc). |
expires |
int |
The expiration time of the access_token . |
refresh_token |
string |
A long, opaque value used for refreshing the access_token . |
resource_owner_name |
string |
An optional symbolic identifier of the person/agent who authorized access. Semantics vary by provider. |
resource_owner |
array |
Any information we have about the person/agent who authorized access. Structure varies by provider. |
tag |
string |
Optional, freeform string which may be used to describe, categorize or identify a token. When tag is set in a call to OAuthClient.authorizationCode , any token that is created as a result will have the tag set accordingly. Some tag values have special meaning when used with the OAuthContactToken storage type (see Contact tokens). |
contact_id |
int |
(OAuthContactToken only) Foreign key to the civicrm_contact table. |
cardinal |
int |
(OAuthSessionToken only) Order in which the token was created in the context of the session. It differs from an "ID" as used most places in CiviCRM, in that "IDs" are typically unique over the lifespan of the CiviCRM instance, whereas cardinal is unique only over the lifespan of the session. |
id |
int |
(OAuthSysToken and OAuthContactToken only) The unique, internal "ID" assigned by MySQL. This is typically a short number. |
Session Tokens¶
These tokens are stored in the user's session.
If the user is logged in, tokens are destroyed when the user logs out, ending the session.
If the tokens are created in the context of an anonymous (non-login) session, they will remain associated with the web browser's interactions with CiviCRM unless they are destroyed (via the APIv4 delete
action) or the session expires.
Either way, you are strongly advised to carefully manage the lifecycle of the OAuthSessionToken
s you create, doing proactive garbage collection and/or obtaining tokens with a short time to live.
Contact Tokens¶
A contact can have any number of Contact Tokens attached to it via the contact_id
foreign key on the tokens.
When you use the API4 OAuthClient
authorizationCode
action to initiate an authorization code flow and specify OAuthContactToken
as the storage type, you can include instructions for how to link the resulting token to a contact.
- If you set
tag
tonullContactId
, the resulting token'scontact_id
will be assignednull
. - If you set
tag
tocreateContact
, Civi will create a new contact and link the token to it. In this case, you must specify acontactTemplate
when you define your Provider so that Civi knows what to put in the new contact record. - If you set
tag
to "linkContact:
" followed by a valid existing contact ID (e.g.linkContact:1234
), Civi will link the resulting token to that contact. - If
tag
is none of the above,contact_id
will be set to the contact ID of the currently logged-in user. If the session is anonymous (not logged in), the contact ID will be null.
Permissions¶
The following CiviCRM permissions pertain to OAuth:
Permission | Description and notes |
---|---|
manage OAuth client | Create and delete OAuth client connections, including the client secret . |
manage OAuth client secrets | Access the access_token and refresh_token fields of System Tokens. Despite the name, this permission does not currently affect a user's ability to view/edit the secret field of an OAuthClient . |
create OAuth tokens via auth code flow | Create OAuth tokens (of any storage type) via the authorization code flow. Some use cases may call for this permission to be given to anonymous users — these use cases most likely involve Contact Tokens or Session Tokens. Other OAuth-related permissions should not be given to anonymous users. |
manage my OAuth contact tokens | A logged in user who has this permission can use API4 to create Contact Tokens associated with their own contact ID, and read/update/delete those tokens if they have at least view access to their own contact record. Other contacts' OAuthContactToken records are invisible to the user. |
manage all OAuth contact tokens | Manage OAuth Contact Tokens for all contacts via API4. |
Tutorial¶
To develop a new integration that relies on a remote OAuth2 service provider, you will need to go through a few steps:
Define a provider¶
For any remote service provider that you wish to interact with, there should be an OAuthProvider
record. For example,
at time of writing, the ms-exchange
provider is defined by a JSON file which specifies:
// FILE: ext/oauth-client/providers/ms-exchange.dist.json
{
"title": "Microsoft Exchange Online",
"class": "Civi\\OAuth\\CiviGenericProvider",
"options": {
"urlAuthorize": "https://login.microsoftonline.com/{{tenant}}/oauth2/v2.0/authorize",
"urlAccessToken": "https://login.microsoftonline.com/{{tenant}}/oauth2/v2.0/token",
"urlResourceOwnerDetails": "{{use_id_token}}",
"scopeSeparator": " ",
"scopes": [
"https://outlook.office.com/IMAP.AccessAsUser.All",
"https://outlook.office.com/POP.AccessAsUser.All",
"https://outlook.office.com/SMTP.Send",
"openid",
"email",
"offline_access"
],
"tenancy": true
}
}
Dedicated Microsoft services require a tenant ID, which can be input when registering the OAuth client entity (see above). CiviCRM will
automatically replace the {{tenant}}
token with the provided ID, or 'common' where none has been set (ie: a consumer account).
Here are a few ways to define a provider:
- To add core support for a provider: Create a JSON file in
[civicrm.root]/ext/oauth-client/providers/*.json
- To temporarily add a provider on the local system: Create a JSON file in
[civicrm.private]/oauth-providers/*.json
- To add a provider to an extension: Implement hook_civicrm_oauthProviders
After adding or modifying a provider, clear the cache (cv flush
).
You can inspect the list of available providers via APIv4 (OAuthProvider
), e.g.
cv api4 OAuthProvider.get -T +s name,title
+-------------+----------------------------+
| name | title |
+-------------+----------------------------+
| gmail | Google Mail |
| ms-exchange | Microsoft Exchange Online |
| demo | Local Demo |
+-------------+----------------------------+
Create a client¶
As presented in CiviCRM System Administrator Guide: Setup: OAuth, there is a
generic administrative interface for registering clients. Alternatively, if you wish to register the client programmatically, then use
APIv4's OAuthClient
.create
:
$client = civicrm_api4('OAuthClient', 'create', [
'provider' => 'NAME',
'guid' => 'CLIENT_ID',
'secret' => 'CLIENT_SECRET',
])->single();
Grant access¶
After registering the client and provider, the client should request access to specific resources. RFC 6749 defines multiple ways to request access:
- Get access to a resource on behalf of a user using a web-based authorization form. If authorized, this yields an "authorization-code". This is the "authorization-code grant-type".
- Get access to a resource on behalf of a user using a username/password. This is the "user-password grant-type".
- Get access to a resource on behalf of the non-human client-agent. This only requires the
client_id
andclient_secret
registered earlier. This is a "client credentials grant-type".
In all three cases, the ultimate goal is to retrieve and store an access token (and, most likely, a corresponding refresh token).
Many OAuth providers regard the "authorization code grant" as the more secure paradigm — and they design policies to encourage use of "authorization code grant". For example, some APIs may only be available with "authorization code grant". Of course, if "client credentials grant" or "password grant" are accepted, then they may allow a more fluid user-experience or simpler process-automation. Ultimately, you will have to consult the provider documentation to determine the appropriate grant-type.
Once you've determined the appropriate grant type, you can use the corresponding API action:
Grant access via authorization code
## Begin authorizationCode grant via PHP
$start = civicrm_api4('OAuthClient', 'authorizationCode', [
'where' => [['id', '=', 123]],
'landingUrl' => CRM_Utils_System::url('civicrm/page/to/come/back/to', NULL, TRUE, NULL, FALSE),
])->single();
CRM_Utils_System::redirect($start['url']);
## Alternatively, begin authorizationCode grant via CLI
cv api4 OAuthClient.authorizationCode +w id=123
In the most well-known OAuth2 flow, one directs the user's browser to visit a remote web-service. They will confirm that they wish to grant permission — and then redirect back to CiviCRM.
Note that the API call does not immediately produce a valid token. Instead, it returns $start['url']
— you
need to redirect the user to this URL.
After they confirm access, the resulting token is stored for usage. (By default, it is stored in OAuthSysToken
.)
Additionally, you may define more steps by:
- Passing a
landingUrl
(as above). This will be the penultimate screen after successful authorization. - Implementing hook_civicrm_oauthReturn and/or hook_civicrm_oauthReturnError
Grant access via username/password
## Perform userPassword grant via PHP
$token = civicrm_api4('OAuthClient', 'userPassword', [
'where' => [['id', '=', 123]],
'username' => 'johndoe',
'password' => 'abcd1234',
])->single();
## Alternatively, perform userPassword grant via CLI
cv api4 OAuthClient.userPassword +w id=123 username=johndoe password=abcd1234
As with "client credentials", this requires access to a specific OAuthClient
. Additionally, it requires
a username and password for some user.
If successful, the resulting token is stored for usage. (By default, it is stored in OAuthSysToken
.)
Grant access via client credentials
## Perform clientCredentials grant via PHP
$token = civicrm_api4('OAuthClient', 'clientCredentials', [
'where' => [['id', '=', 123]],
])->single();
## Alternatively, perform clientCredentials via CLI
cv api4 OAuthClient.clientCredentials +w id=123
Technically, this flow requires access to the client_id
(guid
) and client_secret
(secret
). Those values are loaded from the
matching client (#123
). This is the simplest form of authentication because it does not require any extra proof to get access.
If successful, the resulting token is stored for usage. (By default, it is stored in OAuthSysToken
.)
In each grant-type, there are a few extra options that can be mixed in:
Assign a tag to the token
In each of the above examples, we request a token and store it. But how will we lookup the token in the future?
One simple mechanism is to assign a tag to the token, e.g.
$result = civicrm_api4('OAuthClient', '...grantType...', [
...
'tag' => 'foobar',
...
])->single();
In the future, when you need access to the token, you can locate it by tag, e.g.
$tokens = civicrm_api4('OAuthSysToken', 'get', [
'where' => [['tag', '=', 'foobar']]
]);
This is useful for some simple cases — e.g. where a token only has a single use. If a token may be re-used in multiple ways, then tagging may not be sufficient — as an alternative, consider storing your own foreign-key reference as-needed.
Choose the OAuthClient
by type
In each of the above examples, the access was granted on behalf of a specific OAuthClient (#123
):
$result = civicrm_api4('OAuthClient', '...grantType...', [
'where' => [['id', '=', 123]],
])->single();
But how do you know to use #123
? Perhaps the user chose #123
from a list? Or perhaps there was a setting?
Of course, if there is only one plausible answer (ie there's a 1:1
relationship between your OAuthProvider
and your OAuthClient
),
then we don't have to bother with that question. Instead, lookup the OAuthClient
by type:
$result = civicrm_api4('OAuthClient', '...grantType...', [
'where' => [['provider', '=', 'gmail']],
'orderBy' => ['id' => 'DESC'],
'limit' => 1,
])->single();
Choose specific scopes
What do you request access to?
In each of the above examples, it uses the list of scopes registered for this provider. However, you may request access using a more fine-tuned list:
$result = civicrm_api4('OAuthClient', '...grantType...', [
...
'scopes' => ['openid', 'read_timeline', 'add_comment'],
...
])->single();
Use a token¶
Once access has been granted, there is a stored token record (OAuthSysToken
, OAuthSessionToken
or OAuthContactToken
). To use the
token, you should first look it up via APIv4.
The conventional way to load a record in APIv4 is with the action get
. This is useful for inspecting the current status of the token.
However, OAuth tokens have an expiration-time built-in, and get
may sometimes return a stale token. The alternative action refresh
is very similar to get
— but it adds support for automatic refreshing. As of this writing, only OAuthSysToken
s support the refresh
action.
Lookup token by ID (no refresh)
$tokenRecord = civicrm_api4('OAuthSysToken', 'get', [
'checkPermissions' => FALSE,
'where' => [['id', '=', '123']]
])->single();
Lookup token by tag (no refresh)
$tokenRecord = civicrm_api4('OAuthSysToken', 'get', [
'checkPermissions' => FALSE,
'where' => [['tag', '=', 'foobar']]
])->single();
Lookup token by ID (auto refresh)
$tokenRecord = civicrm_api4('OAuthSysToken', 'refresh', [
'checkPermissions' => FALSE,
'where' => [['id', '=', '123']]
])->single();
Lookup token by tag (auto refresh)
$tokenRecord = civicrm_api4('OAuthSysToken', 'refresh', [
'checkPermissions' => FALSE,
'where' => [['tag', '=', 'foobar']]
])->single();
By default, refresh
is lazy — it only refreshes the token if it has expired (or if it will expire within 60 seconds). You can
optionally tune this behavior.
Auto-refresh with 5 minute threshold
Look up a token. Automatically refresh if it has less than 5 minutes of validity:
$tokenRecord = civicrm_api4('OAuthSysToken', 'refresh', [
'checkPermissions' => FALSE,
'where' => [['id', '=', '123']]
'threshold' => 5 * 60,
])->single();
Auto-refresh — no matter what
Look up a token. Automatically refresh, regardless of whether it has expired.
$tokenRecord = civicrm_api4('OAuthSysToken', 'refresh', [
'checkPermissions' => FALSE,
'where' => [['id', '=', '123']]
'threshold' => -1,
])->single();
When you have a reference to a fresh $tokenRecord
, you can use $tokenRecord['access_token']
with your favorite
client library, such as guzzlehttp/guzzle or league/oauth2-client.
Use a token with Guzzle HTTP
In this example, we create a new instance of GuzzleHttp\Client
.
$client = new GuzzleHttp\Client([
'base_uri' => 'https://openidconnect.googleapis.com/v1/',
'headers' => [
'Authorization' => 'Bearer ' . $tokenRecord['access_token'],
'Accept' => 'application/json',
],
]);
echo $client->get('userinfo')->getBody();
The $client
has various defaults (such as base_uri
and the Authorization
header) to ensure that any calls to get()
,
post()
, request()
, etc are suitably authenticated.
Use a token with the PHP League provider object
If you recall from Model: Provider, CiviCRM's OAuth is based on the PHP League's oauth2-client. In their model, one may use a generic provider class — or a specialized provider class. These classes may define extra helper methods for interacting with the provider.
Given a stored token, you may instantiate the corresponding $provider
and $accessToken
objects:
$lg = \Civi::service('oauth2.league')->create($tokenRecord);
$request = $lg['provider']->getAuthenticatedRequest('GET', 'https://service.example.com/resource',
$lg['token']
);
Putting these together, we can make a more complete example screen:
Lookup token for Gmail and request user info
// Get a reference to the most recent 'gmail' token.
$tokenRecord = civicrm_api4('OAuthSysToken', 'refresh', [
'checkPermissions' => FALSE,
'where' => [['client.provider', '=', 'gmail']],
'orderBy' => ['id' => 'DESC'],
'limit' => 1,
])->single();
// Create a Guzzle client using this token
$client = new GuzzleHttp\Client([
'base_uri' => 'https://openidconnect.googleapis.com/v1/',
'headers' => [
'Authorization' => 'Bearer ' . $tokenRecord['access_token'],
'Accept' => 'application/json',
],
]);
// Make specific request with the token.
echo $client->get('userinfo')->getBody();