Общая информация по JWT потоку OAuth
JSON Web Token (JWT, произносится как английское слово jot) это зашифрованный JSON секюрити токен (security token), который делает возможной идентификацию и предоставление защищенного доступа к информации со стороннего домена.
Пропустить теорию.
OAuth 2.0 JWT Bearer Token поток определяет как JWT-токен может быть использован для запроса OAuth access токен из Salesforce, когда клиентская сторона хочет воспользоваться ранее пройденой авторизацией. Данные входящие в JWT содержат имя пользователя (username), для которого нужен access токен, а так же идентификатор приложения, которое запрашивает доступ (client_id). Аутентификация запращивающего приложения предоставляется посредством применения цифровой подписи к JWT — т.е. json токен должен быть закодирован c помощью RSA ключа.
JWT Bearer Token поток поддерживает RSA/SHA256 алгоритм, т.е. вы должны загрузить подписаный сертификат в на странице конфигурации вашего Connected App, это позволит Salesforce удостовериться в том что ваш токен пришел из доверенного источника.
При использовании JWT Bearer Token потока, сам JWT-токен отсылается c помощю POST запроса (1) на url называемый OAuth endpoint — login.salesforce.com/services/oauth2/token и содержит следующие данные (payload)
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=PHN...QZT
grant_type | Set this to urn:ietf:params:oauth:grant-type:jwt-bearer |
assertion | The JWT bearer token. |
Сервер проверяет JWT и создает токен доступа (access_token) (2), на основании ранее предоставленного приложению доступа. Однако, клиентская сторона не обязана хранить refresh_token (как при других OAuth потоках), в данном случае клиентское приложение генерирует новый JWT-токен когда необходимо получить access_token. Так же обратите внимание, что client_secret не является необходимым при отсылке запроса на получение токена — JWT авторизует приложение.
Больше теории про OAuth 2.0 JWT Bearer Token поток и посмотреть пример на java можно прочитать здесь — OAuth 2.0 JWT Bearer Token Flow.
Генерация приватного ключа и сертификата
JWT Bearer Flow требует RSA SHA256 сертификата (X.509). Сгенерировать это пару можно с помощью openssl утилиты. Ниже пример коммандной строки для генерации ключа и сертификата
openssl req -x509 -nodes -subj "/C=US/ST=CA/L=San Francisco/CN=web_site.com" -days 3650 -newkey rsa:2048 -keyout your_private_key.pem -out your_certificate.pem
your_private_key.pem — будет использован в клиентском приложении
your_certificate.pem — будет загружен в Connected app на salesforce (куда вы будете авторизоваться)
Внимание! Ключ и сертификат указаные ниже просто пример. Не вздумайте использовать их в продакшне.
your_private_key.pem
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA9aHYIsb3WWWfhSeDdZnZagxRrHvcam1QZc5QCGr1EyiuBedb
9xCdJx9qXKv7qvF0fTxZU5wUVicgvUYrTg9hBxx2rkZ4GKgy6DQSO3A/d/7CUk/d
6BrIQlYLW5iLZ68aN7RoVdTl8+wMRjW6yuAIl02bpeI1yuLFDL7jM9mA+PEtSRcB
4JrbvfKoiyG8Q0PWXpl1DyjpqjmP02ezGlUe9MwiZQ6w3MfR0tMOAaroYKo+Tpqp
yDy98ca6cwOr1G7H46n/ifITJp1QyWPUA8P0/Bu/R2nLhsec5Xp5FOJgIn4tOysJ
oAaZRAmg+OMMn7IosiOOtzxWOFROYP89zUwICQIDAQABAoIBAQDSAuNJsLXQ0EtY
TFVgWf8CZa/6+heVQwtHT+Mrn08UZ2aKk6oUjjbfOUP2IfFV0YWYgd2Z18uncWLT
kUf7QLjjnJl4HbYW1tBXqhWgjjMlxEKen7yJ80QfO3QQh1dBefu/ftbp89H8AGjq
KowfYvbVbjxS0xrM8aAqapbVQPS9YcJGm4VvzE+H+p/cdORfmcssBZtX8ym/EeGB
hOWmmQyHxrfR5SzxWyGwntT7kMbZMJ5rixiUnDUhHgTSOWbKYTKPE5sojrV8en3r
Ic/Sy/GqwHPfDhaA/ZfPI7d8wBueDrMOb4goO6oNmO6xQkozSOgIsOmpHOpkEaj/
Vlbqmw7RAoGBAP+GLKFSPSgKt5ZymRieZmPXAYohYIcDAExSYdQOA6vVlheXn9Kr
/fHZLNult0omevCb7stW192H2A2RN5HzqUO8eJ0/DyUR13ZluNGUUxLMh//wapKT
wfCXzDMYdYoeVf6PgZwLdY2BSXG3/Ycr7N27ak251Nffgnh4RxXW8eL1AoGBAPYW
9CweiJ1Vt5Z3KHDRsoCWqFYH+229j9tcxdJGt5VNKrkDxOJfhGu3N6dESo8qEGuu
ix6hZhMnF04s8oiBowXFfoopw4K3CUR9paaM+W1fsKDM0f51wZAxd2ZmKnNZW53a
zc5iQH0Zy1qT9M5iiXM3VutadSkwupETlOqRYOxFAoGAEf5Z0DZhVhuDGBYTz9b/
sNIoKpj1GizM7ZLzjqI6AfS1cA3eVFCGPmyjqwf9YzxYde8VHr6Lzu7M+Q+b0SxO
ZBW2jKQvJdYezRiWrjN6sh0zCoPcjVvYUV/vIj37sPE37wgeAWYRLhjHmjlxof4m
3Opgrv6CDX2Qy3j00IXlXK0CgYAU9smgRI2g0Z+NKuOAEO0i9TKr+Ywawi5SIqob
iriy+FruXfrUygxO3NHZ5wBvB8dUVQ828crvUMI0f7G9nUWVBUkNXhdwuEUK16VX
9eR9w8wZNrmg8skljoE8cPGm1/LtFKm5rjcOMTdYpQgS2OQas5ks/YzDkIokN8XU
4cOe/QKBgFp2xSur5d4OFm3yfwq9I2p+tLQrKQexgPExbs4opPmGvsg429xmiLDs
d1V0622jOeZflsSFJpsY/MPgxMM2mEfjfEWmqcyOHKoCGbsBc4HC7UJ5zXo4Txaz
1dU9+mRV9XS7mr5bSgNyRB3X1DEeJ4Wi8tp3gV4FH0DwSNGByS/1
-----END RSA PRIVATE KEY-----
your_certificate.pem
-----BEGIN CERTIFICATE-----
MIIDwTCCAqmgAwIBAgIJAOuaqw3NQvmyMA0GCSqGSIb3DQEBBQUAMEkxCzAJBgNV
BAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEVMBMG
A1UEAxQMd2ViX3NpdGUuY29tMB4XDTE0MTIxODE0MzkxM1oXDTI0MTIxNTE0Mzkx
M1owSTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJh
bmNpc2NvMRUwEwYDVQQDFAx3ZWJfc2l0ZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQD1odgixvdZZZ+FJ4N1mdlqDFGse9xqbVBlzlAIavUTKK4F
51v3EJ0nH2pcq/uq8XR9PFlTnBRWJyC9RitOD2EHHHauRngYqDLoNBI7cD93/sJS
T93oGshCVgtbmItnrxo3tGhV1OXz7AxGNbrK4AiXTZul4jXK4sUMvuMz2YD48S1J
FwHgmtu98qiLIbxDQ9ZemXUPKOmqOY/TZ7MaVR70zCJlDrDcx9HS0w4Bquhgqj5O
mqnIPL3xxrpzA6vUbsfjqf+J8hMmnVDJY9QDw/T8G79HacuGx5zlenkU4mAifi07
KwmgBplECaD44wyfsiiyI463PFY4VE5g/z3NTAgJAgMBAAGjgaswgagwHQYDVR0O
BBYEFComQlVarS6Y5vru8+0WVLEN5fkeMHkGA1UdIwRyMHCAFComQlVarS6Y5vru
8+0WVLEN5fkeoU2kSzBJMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNV
BAcTDVNhbiBGcmFuY2lzY28xFTATBgNVBAMUDHdlYl9zaXRlLmNvbYIJAOuaqw3N
QvmyMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAN4bpIEbkYe/fkMZ
gLplj5wVz4CQgTeYdjgQnZqlJV7bXUJnEREE1oyfS5UHHvKpAOzBhZFNc+T0LFV1
ivKBa6ZJnMY9iUqDuyf7x3QyV62bWeHqOuT8RcZpMtJ1YcQUZgotWlvm2/4zlbVE
sZNhsC8NxPRGu3T9d+GPa2zfICMMwDGOHJejPH0FBxdbByHDRkg59UYLFWhejY1G
qN2QRDOi6B8LytcMkMyiPZoXL8mFOhR9C6GdoLcc1IZZk5PnrAn0i8sgBs9d4ev1
JU42f8cI8QSG/xKftdVRsoSBPG+EGXJ4EmjUXt1XMu9zSij5WZP5mHfzdfkLgwnf
XFhp0s0=
-----END CERTIFICATE-----
Конфигурация salesforce орга
- Setup->Create-> App -> scroll down to Connected Apps -> click New
- Конфигурация Connected app и загрузка сертификата сгенерированого на первом шаге
- Сохранить измениния.
- Открыть страницу настройки приложения Setup -> Manage Apps -> Connected App -> выбрать ваше приложение и нажать кнопку Edit
- Выбрать опцию All users may self-authorize for Permitted Users
- Выбрать опцию Relax IP Restrictions for IP Restrictions
- Нажать Save
Процесс авторизации с использованием JWT
Для того чтобы разрешить соединение из одной salesforce организации к другой salesforce организации нужно прописать Remote Site Settings на стороне клиента.
Setup -> Security controls -> Remote Site Settings -> New
Чтобы разрешить предоставить доступ определенному приложению для определенного пользователя, надо будет скомпоновать и открыть в браузере определенный линк. Формат у него следующий:
https://login.salesforce.com/services/oauth2/authorize?client_id=[client_id]&redirect_uri=[redirect_url]&response_type=code
client_id | т.н. консьюмер ключ (consumer key) сгенерированый при создании Connected App, в нашем случае — 3MVG9QDx8IX8nP5RvIRcKYiylTgtQ1RUJT7tgOC0REp4bEA3JwFM5t39RGF_H5Kwfh6hf_j3.XKTf.IMOTw1Q |
redirect_url | урл на который будет происходить редирект (он тоже должен соответствовать тому, что было указано при создании Connected App), в нашем случае login.salesforce.com/home/home.jsp |
Чтобы получить токен ноступа (access token) мы должны будем создать JWT и подписать его с помощью RSA SHA256 ключа, сгенерпрованого на первом шаге.
Ниже код на Apex для генерации JWT и получения access_token
public with sharing class JwtAuth {
//JWT Header
public class JwtHeader{
public String alg = 'RS256';
}
//JWT Claims Set
public class JsonClaimsSet{
public String iss; //Consumer Key of Salesforce Connected App
public String prn; //Salesforce username on destination Org
public String aud = 'https://login.salesforce.com'; //https://login.salesforce.com OR https://test.salesforce.com
public String exp = '' + System.currentTimeMillis() + (1000 * 60 *60); //Token expiration date - timestamp in miliseconds
public String sub; //optional in JWT Bearer Token flow
}
// JWT Auth flow result
public class JwtAuthResult {
public String scope;
public String instance_url;
public String token_type;
public String access_token;
public String error;
public String error_description;
}
// Singleton
private static JwtAuth self;
private JwtAuth () {}
public static JwtAuth getInstance() {
if (self == null) {
return new JwtAuth();
}
return self;
}
public String generateJWT(String consumerKey, String sfUsername, String x509PrivateKey) {
//lifetime by defaultt = 10 minutes
//connect to production by default
return generateJWT(consumerKey, sfUsername, x509PrivateKey, 600, false);
}
private String generateJWT(String consumerKey, String sfUsername, String x509PrivateKey, Integer lifetimeSeconds, Boolean isSandbox) {
//1. Construct a JWT Header
JwtHeader header = new JwtHeader();
header.alg = 'RS256';
String jwtHeaderStr = JSON.serialize(header);
jwtHeaderStr = base64EncodeUrl(jwtHeaderStr);
//2. Construct a JSON Claims Set
JsonClaimsSet claimsSet = new JsonClaimsSet();
claimsSet.iss = consumerKey;
claimsSet.prn = sfUsername;
claimsSet.aud = isSandbox ? 'https://test.salesforce.com' : 'https://login.salesforce.com';
claimsSet.exp = '' + (System.currentTimeMillis() + lifetimeSeconds*1000);
String jsonClaimsSetStr = JSON.serialize(claimsSet);
jsonClaimsSetStr = base64EncodeUrl(jsonClaimsSetStr);
//3. Join JWT Header and JSON Claims Set
String token = jwtHeaderStr + '.' + jsonClaimsSetStr;
//4. Sign with RSA-SHA256 Certificate
String signedPayload = base64EncodeUrl(
Crypto.sign('RSA-SHA256', Blob.valueOf(token), EncodingUtil.base64Decode(x509PrivateKey))
);
return token + '.' + signedPayload;
}
public JwtAuthResult requestAccessToken (String endpoint, String jwtSignedToken) {
HttpRequest req = new HttpRequest();
req.setEndpoint(endpoint);
req.setBody(
EncodingUtil.urlDecode('grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer', 'UTF-8')
+ '&assertion=' + jwtSignedToken);
req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
req.setMethod('POST');
Http http = new Http();
HTTPResponse res = http.send(req);
String result = res.getBody();
return (JwtAuthResult) JSON.deserialize(result, JwtAuth.JwtAuthResult.class);
}
//Convert Plain base64 to base64Url
public String base64EncodeUrl(Blob data) {
return EncodingUtil.base64Encode(data).replaceAll('\+','-').replaceAll('/','_');
}
//Convert Plain base64 to base64Url
public String base64EncodeUrl(String data) {
return EncodingUtil.base64Encode(Blob.valueOf(data)).replaceAll('\+','-').replaceAll('/','_');
}
}
Полезные ресурсы по теме:
Digging Deeper into OAuth 2.0 on Force.com
Remote Access. Oauth JWT flow
Apex Crypto Documentation
Apex Crypto Examples
Автор: yurybond