Salesforceでは、「Salesforce で Client Credentials フローな指定ログイン情報を使いたい」で紹介したように、「認証プロバイダ」を利用して、外部システムとの認証・シングルサインオンを容易にしてくれる機能があります。
が、しかし、これらは次の外部認証プロバイダのみのサポートとなっています。
Amazon- Apple
- GitHub (New)
- Janrain
- Microsoft アクセスコントロールサービス
Microsoft Azure AD- Salesforce
- OpenID Connect プロトコルを実装しているサービスプロバイダ
(Winter'22現在)
よーく「外部認証プロバイダの設定」を見ると、OAuth をサポートしているが OpenID Connect プロトコルをサポートしていないサービスプロバイダというものがあるので、なんだ「OAuth」対応してるじゃんと思ってリンクを踏むと「カスタム外部認証プロバイダの作成」と書いてあるではありませんか。そう、OAuth
の認証プロバイダは、現時点では己の手でソースコードレベルで作成が必要です。
しかも、ここを読んでも「カスタムメタデータ型の作成」と「Apexクラスおよびメソッドの作成」しか書いてありません。じゃぁ...ということで、「SSO のカスタム認証プロバイダを作成するには、Auth.AuthProviderPluginClass 抽象クラスを拡張するクラスを作成します」という言葉から「Auth.AuthProviderPluginClass
」を調べてみるとカスタム認証プロバイダプラグインの作成が見つかるでしょう。しかし、ここを読んでも、何をしたらいいかを理解するのは難しいと判断します。いやだって難しかったもん。
ということで、今回Client Credential
向けに準備したソースコードをもとに、何を準備したら良いかと説明しましょう。
ちなみに、全ソースコードはこちら。
global class ClientCredentialsAuthProvider extends Auth.AuthProviderPluginClass {
// define
public static final String CUSTOM_MDT_NAME = 'ClientCredentials_Metadata__mdt';
public static final String CALLBACK_URL = '/services/authcallback/';
public static final String GRANT_TYPE = 'client_credentials';
public static final String CMDT_FIELD_PROVIDER_NAME = 'Auth_provider__c';
public static final String CMDT_FIELD_ACCESS_TOKEN_URL = 'Access_Token_URL__c';
public static final String CMDT_FIELD_CLIENT_ID = 'Client_ID__c';
public static final String CMDT_FIELD_CLIENT_SECRET = 'Client_Secret__c';
public static final String CMDT_FIELD_USER_NAME = 'User_Name__c';
// access token response
private class TokenResponse {
public String access_token; //required
public String token_type; //required
public Long expires_in; //recommended
public String refresh_token; //optional
public String scope; //optional
public String error; //in case of error
public String error_description;//in case of error
public String error_uri; //in case of error
public Boolean isError() {
return error != null;
}
}
global String getCustomMetadataType() {
return CUSTOM_MDT_NAME;
}
global PageReference initiate(Map<string,string> authProviderConfiguration, String stateToPropagate) {
final PageReference pageRef = new PageReference(getCallbackUrl(authProviderConfiguration));
pageRef.getParameters().put('state',stateToPropagate);
return pageRef;
}
private String getCallbackUrl(Map<string,string> config) {
return URL.getSalesforceBaseUrl().toExternalForm() + CALLBACK_URL + config.get(CMDT_FIELD_PROVIDER_NAME);
}
global Auth.AuthProviderTokenResponse handleCallback(Map<string,string> authProviderConfiguration, Auth.AuthProviderCallbackState callbackState) {
TokenResponse tokenResponse = retrieveToken(authProviderConfiguration);
if (tokenResponse.isError()) {
throw new TokenException(tokenResponse.error);
}
return new Auth.AuthProviderTokenResponse(
authProviderConfiguration.get(CMDT_FIELD_PROVIDER_NAME), // provider
tokenResponse.access_token, // oauthToken
'refreshToken', // oauthSecretOrRefreshToken
callbackState.queryParameters.get('state') // state
);
}
global Auth.UserData getUserInfo(Map<string,string> authProviderConfiguration, Auth.AuthProviderTokenResponse response) {
String userName = authProviderConfiguration.get(CMDT_FIELD_USER_NAME);
String provider = authProviderConfiguration.get(CMDT_FIELD_PROVIDER_NAME);
return new Auth.UserData(
null, // identifier
null, // firstName
null, // lastName
null, // fullName
userName, // email
null, // link
userName, // userName
null, // locale
provider, // provider
null, // siteLoginUrl
new Map<String,String>()
);
}
override global Auth.OAuthRefreshResult refresh(Map<String,String> authProviderConfiguration, String refreshToken) {
TokenResponse tokenResponse = retrieveToken(authProviderConfiguration);
return new Auth.OAuthRefreshResult(tokenResponse.access_token, tokenResponse.token_type);
}
private TokenResponse retrieveToken(Map<String,String> config) {
String access_token_url = config.get(CMDT_FIELD_ACCESS_TOKEN_URL);
String parameters = 'client_id=' + config.get(CMDT_FIELD_CLIENT_ID)
+ '&client_secret=' + config.get(CMDT_FIELD_CLIENT_SECRET)
+ '&grant_type=' + GRANT_TYPE;
HttpRequest req = new HttpRequest();
req.setEndpoint(access_token_url);
req.setHeader('Content-Type','application/x-www-form-urlencoded;charset=UTF-8');
req.setMethod('POST');
req.setBody(parameters);
Http http = new Http();
HTTPResponse res = http.send(req);
return (TokenResponse)JSON.deserialize(res.getBody(), TokenResponse.class);
}
public class TokenException extends Exception { }
}
変数定義部分の説明
抽象型 Class を継承する話は置いといて、このClientCredentialsAuthProvider
の各項目について、説明します。ここで extends
した Class名はプロバイダタイプ
になります。
変数定義の最初の部分は定数定義です。分かりやすいように名前をつけているだけですね。影響がある部分はCUSTOM_MDT_NAME
ですね。ここはカスタムメタデータ型
で定義したAPI名になります。「外部認証プロバイダ」で使用するClient ID
やClient Secret
などは、このカスタムメタデータ型で定義したレコードに保管します。
当Client Credentials型で必要なカスタムメタデータ型は次のとおりです。
- Client_ID ... (Salesfroceでいう)クライアント鍵のこと
- Client_Secret ... (Salesforceでいう)クライアントの秘密のこと
- Access_Token_URL ... 外部の認証サーバで Client Credentials を受け付ける URL
- Auth_Provider ... 自身のプロバイダ名
- User_Name ... Salesforce 側の利用者のメールアドレス
User_Name
は、実際の稼働には必要がありません。Client Credentials型では、相手側からユーザ情報は返却されません。認証されたトークンなどの一部の情報だけです。それだとちょっとさみしいので、ユーザ名(メールアドレス)を指定して、Salesforce ユーザと連携できた風にしています。
// define
public static final String CUSTOM_MDT_NAME = 'ClientCredentials_Metadata__mdt';
public static final String CALLBACK_URL = '/services/authcallback/';
public static final String GRANT_TYPE = 'client_credentials';
次の定数は、先程のカスタムメタデータ型で指定したフィールド名です。カスタムメタデータ型で定義された、個々のレコードの内容を取得するときに利用します。
public static final String CMDT_FIELD_PROVIDER_NAME = 'Auth_provider__c';
public static final String CMDT_FIELD_ACCESS_TOKEN_URL = 'Access_Token_URL__c';
public static final String CMDT_FIELD_CLIENT_ID = 'Client_ID__c';
public static final String CMDT_FIELD_CLIENT_SECRET = 'Client_Secret__c';
public static final String CMDT_FIELD_USER_NAME = 'User_Name__c';
次は、一般的に Client Credentials で返却されるフォーマットをオブジェクト型にしたものです。//required
部分が必ず返却される内容で、error
部分はエラーが発生したとき以外は、特に返却されないことがほとんどな気がします。
Client Credentials 型で、相手のサーバと接続するために必要なのは、ここでいうaccess_token
だけです。相手のサーバがどのような値を返却するかにもよりますが、最大をとっておけば抜け漏れることもないので、最大の状態で定義しています。
それと isError()
メソッドを付け加えています。エラーがあったかどうかを調べるためだけのメソッドです。
// access token response
private class TokenResponse {
public String access_token; //required
public String token_type; //required
public Long expires_in; //recommended
public String refresh_token; //optional
public String scope; //optional
public String error; //in case of error
public String error_description;//in case of error
public String error_uri; //in case of error
public Boolean isError() {
return error != null;
}
}
メソッド部の説明
カスタム認証プロバイダを作成する場合にglobal
で指定された、次の4つのメソッドの作成は必須です。
global String getCustomMetadataType()
global PageReference initiate(Map<string,string>, String)
global Auth.AuthProviderTokenResponse handleCallback(Map<string,string>, Auth.AuthProviderCallbackState)
global Auth.UserData getUserInfo(Map<string,string>, Auth.AuthProviderTokenResponse)
最低限必須のメソッドの内容について、まず説明します。
String getCustomMetadataType()
このメソッドはカンタンです。定義したカスタムメタデータ型を返却すればよろしい。見たままの内容をreturn
してください。
必要なのは、認証プロバイダで定義されるコールバックURLです。
global String getCustomMetadataType() {
return CUSTOM_MDT_NAME;
}
global PageReference initiate(Map, String)
ユーザが認証されるときのリダイレクト先のURLを返却してあげます。このURLは、Client Credentials
の場合は、次のとおりになります。
自ホスト名
+ /services/authcallback/
+ 認証プロバイダ名
private
で定義した、getCallbackUrl()
でこれらを組み立てています。
もう一つ、Salesforce システムが認証要求を開始するための「状態」をパラメータに付与しておく必要があります。それがpageRef.getParameters().put('state',stateToPropagate);
ですね。必須の項目と考えておいて良いです。Salesforceが認証要求を行う際に、このstate
クエリを見て状態の判断を行うので、素直に入れておいてください。
global PageReference initiate(Map<string,string> authProviderConfiguration, String stateToPropagate) {
final PageReference pageRef = new PageReference(getCallbackUrl(authProviderConfiguration));
pageRef.getParameters().put('state',stateToPropagate);
return pageRef;
}
private String getCallbackUrl(Map<string,string> config) {
return URL.getSalesforceBaseUrl().toExternalForm() + CALLBACK_URL + config.get(CMDT_FIELD_PROVIDER_NAME);
}
global Auth.AuthProviderTokenResponse handleCallback(Map, Auth.AuthProviderCallbackState)
実際にサーバへClient Credential
を送って、アクセストークンを取得する部分です。メインの部分ですね。
実際に HTTPコールアウトしている部分は retrieveToken()
メソッドとしていますので、そちらを参考になさってください。tokenResponse
オブジェクトは先程定義したオブジェクトクラスです。
返却されたトークン内にエラーが発生していたらthrow
してエラーを返しています。
成功したら Auth.AuthProviderTokenResponse
オブジェクト型で内容を返却しています。注釈にも書いてあるとおり、次の4つの内容を返却します。
- provider ... プロバイダ名 (固定)
- oauthToken ... 実際に相手のサーバから受け取ったアクセストークンです
- oauthSecretOrRefreshToken ... 現在ログインしているユーザの OAuth の秘密または更新トークンが入りますが、
Client Credentials
の初期アクセス時には利用しないので、適当な文字列でやり過ごします - state ... 先にも示された
stateToPropage
の値です。コールバックURLクエリパラメータ内のstate
を入れておきますから、この内容でセットと組み合わせで覚えておけばよいです
global Auth.AuthProviderTokenResponse handleCallback(Map<string,string> authProviderConfiguration, Auth.AuthProviderCallbackState callbackState) {
TokenResponse tokenResponse = retrieveToken(authProviderConfiguration);
if (tokenResponse.isError()) {
throw new TokenException(tokenResponse.error);
}
return new Auth.AuthProviderTokenResponse(
authProviderConfiguration.get(CMDT_FIELD_PROVIDER_NAME), // provider
tokenResponse.access_token, // oauthToken
'refreshToken', // oauthSecretOrRefreshToken
callbackState.queryParameters.get('state') // state
);
}
private TokenResponse retrieveToken(Map)
もうひとつ必須 global メソッドがありますが、HTTPコールアウトに合わせて、ここで説明します。
HTTPコールアウト部分が、カスタム認証プロバイダでの「キモ」の部分です。Client Credentials
以外のOAuthフローを行う場合に、クエリパラメータの内容が変わるのは、ここの部分です。とは言え、難しい内容ではありません。ここでは、必要なパラメータを一つずつセットして、コールアウトし、結果のJSONを受け取っているだけです。カンタンですよー。
-
access_token_url
... OAuthトークンを受け取るために接続する相手先サーバをセットしています -
parameters
...Client Credentials
で必要なパラメータclient_id
,client_secret
,grant_type=client_credentials
を設定しています。それぞれ、カスタムメタデータ型や定数定義した変数をセットしているだけです
これらアクセス先URLとHTTPヘッダを組み立てたら、content-type
とPOST
メソッドでHTTPコールアウトします。返却された JSONをパースしてTokenResponse
オブジェクト型に組み直してオブジェクトとして return します。このdeserialize
メソッド便利ですね、ありがたい。
private TokenResponse retrieveToken(Map<String,String> config) {
String access_token_url = config.get(CMDT_FIELD_ACCESS_TOKEN_URL);
String parameters = 'client_id=' + config.get(CMDT_FIELD_CLIENT_ID)
+ '&client_secret=' + config.get(CMDT_FIELD_CLIENT_SECRET)
+ '&grant_type=' + GRANT_TYPE;
HttpRequest req = new HttpRequest();
req.setEndpoint(access_token_url);
req.setHeader('Content-Type','application/x-www-form-urlencoded;charset=UTF-8');
req.setMethod('POST');
req.setBody(parameters);
Http http = new Http();
HTTPResponse res = http.send(req);
return (TokenResponse)JSON.deserialize(res.getBody(), TokenResponse.class);
}
global Auth.UserData getUserInfo(Map, Auth.AuthProviderTokenResponse)
最後の必須メソッドが、getUserInfo()
です。外部の認証プロバイダから取得された、現在のユーザ情報を返却します。外部での認証した情報をもとに、Salesforce でのユーザを紐付ける場合に利用します。特に相手からユーザ情報を取得するフローではありませんから、ここではダミーデータを埋めておきます。
返却されるオブジェクト型はAuth.UserData
型で、次の内容になります。
- identifier ... 外部認証サービスが発行するユーザの識別子です。入れなくていいです
- firstName ... 外部認証サービスが発行する、認証済みユーザ名。入れなくていいです
- lastName ... 外部認証サービスが発行する、認証済みユーザ姓。入れなくていいです
- fullName ... 外部認証サービスが発行する、認証済みユーザ氏名。入れなくていいです
- email ... 外部認証サービスが発行する、認証済みユーザのメールアドレス。入れなくていいです
- link ... 外部認証サービスが発行する、認証済みユーザの固定URLリンク。入れなくていいです
- userName ... 外部認証サービスが発行する、認証済みユーザ名。入れなくていいです
- locale ... 外部認証サービスが発行する、認証済みユーザのロケール。入れなくていいです
- provider ... 外部認証サービス名。自分自身のカスタム認証プロバイダ名にしておきます
- siteLoginUrl ... サイトログイン用のURL。入れなくていいです
- attributeMap ... 外部認証サービス固有のパラメータがあれば、ここに紐付けます。入れなくていいです
要するに、何も入れなくていいみたいです。相互認証をするわけではないClient Credentials
なので、ダミーデータを返してあげればいいです。
global Auth.UserData getUserInfo(Map<string,string> authProviderConfiguration, Auth.AuthProviderTokenResponse response) {
String userName = authProviderConfiguration.get(CMDT_FIELD_USER_NAME);
String provider = authProviderConfiguration.get(CMDT_FIELD_PROVIDER_NAME);
return new Auth.UserData(
null, // identifier
null, // firstName
null, // lastName
null, // fullName
userName, // email
null, // link
userName, // userName
null, // locale
provider, // provider
null, // siteLoginUrl
new Map<String,String>() // attributeMap
);
}
override global Auth.OAuthRefreshResult refresh(MapString)
ここまでが必須項目ですが、相手が「リフレッシュトークン」に対応している場合には、refresh()
メソッドをオーバーライドしておけば、トークンの期限切れにもちゃっかり対処できます。
相手先にもよりますが、リフレッシュトークン取得の方式が整っているのであれば、相手に従ってHTTPコールアウトを準備します。が、こちらのように、再度OAuth
の認可フローをまわして再取得することでも同様の動作になります。
返り値は、access_token
とtoken_type
だけで良いです。
override global Auth.OAuthRefreshResult refresh(Map<String,String> authProviderConfiguration, String refreshToken) {
TokenResponse tokenResponse = retrieveToken(authProviderConfiguration);
return new Auth.OAuthRefreshResult(tokenResponse.access_token, tokenResponse.token_type);
}
それ以外に必要な構成・設定についてはnetatmo Weather Station のデータを定期的に Salesforce に取り込んでみたに詳しいので、ここではあくまでもソースコードの内容だけにフォーカスしました( ^ω^ )