2014-01-08

ApexからSTS触ってみる。

例のごとくApexからSTSを触ってみました。

STSとは

STSはAWS Security Token ServiceというAWSのサービスで[IAMユーザを持たないアプリに対してアクセス権限を与える]サービスになります。

具体的には以下の用途で利用されます

WebID連携やSAML連携はAccessKeyIdやSecretAccessKeyを完全に隠蔽できるので、セキュアになるだけでなく上記クレデンシャルの管理をしなくて済むので保守性も良くなります。

開発環境のAWSアカウントと本番環境のAWSアカウントを分けている場合は、クロスアカウントアクセスが有効で、本番環境のクレデンシャル無しに本番環境のリソースにアクセス出来ます。

詳細なユースケースはこちらから↓ http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/WorkingWithRoles.html

ちなみにIAMのRoleっていう機能はイコールSTSなので、STSはIAMユーザを持たないアプリに対してRoleを付与するためのサービスになります。

Apexから叩く

で、以下がApexのサンプルクラス

/**
 * STSコモンクラス
 */
public with sharing class STSCommon {
    private final String SERVICE = 'sts';
    private final String REGION = 'us-east-1';

    /**
     * Access Key Id
     */
    private String AWSAccessKeyId = 'Input Your AWS Access Key Is';

    /**
     * Access Key Secret
     */
    private String AWSAccessKeySecret = 'AWS Secret Access Key';

    public String assumeRole(
        String roleArn, 
        String roleSessionName
    ) {
        Map<String, String> params = new Map<String, String>{

        };

        List<String> signedHeaders = new List<String>{
            'host', 'x-amz-date'
        };

        DateTime dt = DateTime.now();
        String credentialScope = dt.formatGmt('YYYYMMdd') + '/' + 
                                 REGION + '/' + SERVICE + '/aws4_request';

        String httpBody = Utility.getParam(new Map<String, String>{
            'RoleArn' => roleArn,
            'RoleSessionName' => roleSessionName,
            'Version' => '2011-06-15',
            'Action' => 'AssumeRole'
        });

        String stringToSign = this.createStringToSign(
            'AWS4-HMAC-SHA256',
            dt, 
            credentialScope, 
            this.createHashedCanonicalRequest(
                'SHA256',
                'POST',
                '/',
                params,
                new Map<String, String>{
                    'Host' => 'sts.amazonaws.com',
                    'x-amz-date' => dt.formatGmt('YYYYMMdd') + 'T' + dt.formatGmt('HHmmss') + 'Z'
                },
                signedHeaders,
                httpBody
            )
        );

        String signature = this.createSignature(
            'hmacSHA256',
            dt,
            REGION,
            SERVICE,
            Blob.valueOf(stringToSign)
        );

        return this.callCommonSTSCall(
            dt,
            'sts.amazonaws.com',
            credentialScope,
            signedHeaders,
            signature,
            '/',
            httpBody
        );
    }

    public String assumeRoleWithSAML(
        String principalArn,
        String roleArn,
        String samlAssertion
    ) {
        String httpBody = Utility.getParam(new Map<String, String>{
            'PrincipalArn' => principalArn,
            'RoleArn' => roleArn,
            'Version' => '2011-06-15',
            'Action' => 'AssumeRoleWithSAML',
            'SAMLAssertion' => samlAssertion
        });

        HttpRequest req = new HttpRequest();

        req.setEndpoint('https://sts.amazonaws.com');
        req.setMethod('POST');
        req.setBody(httpBody);

        Http http = new Http();
        HTTPResponse res = http.send(req);
        return res.getBody();
    }

    public String createConsoleAccessURL(
        String sessionId,
        String sessionKey,
        String sessionToken
    ) {
        String jsonBody = EncodingUtil.urlEncode(JSON.serialize(new Map<String, Object>{
           'sessionId' => sessionId,
           'sessionKey' => sessionKey,
           'sessionToken' => sessionToken
        }), 'UTF-8');

        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://signin.aws.amazon.com/federation?Action=getSigninToken&Session=' + jsonBody);
        req.setMethod('GET');

        Http http = new Http();
        HTTPResponse res = http.send(req);
        String response = res.getBody();

        Map<String, Object> resMap = (Map<String, Object>)JSON.deserializeUntyped(response);

        String returnUrl = 'https://signin.aws.amazon.com/federation' + 
                           '?Action=login&' + 
                           'Issuer=&' + 
                           'Destination=https%3A%2F%2Fconsole.aws.amazon.com%2F&' + 
                           'SigninToken=' + String.valueOf(resMap.get('SigninToken'));

        return returnUrl;
    }

    /**
     * STSへのAPIコールの共通メソッド
     */
    private String callCommonSTSCall(
        DateTime dt,
        String hostname,
        String credentialScope,
        List<String> signedHeaders,
        String signature,
        String resource,
        String httpBody
    ) {
        HttpRequest req = new HttpRequest();
        req.setHeader('Host', hostname);
        req.setHeader(
            'x-amz-date', 
            dt.formatGmt('YYYYMMdd') + 'T' + dt.formatGmt('HHmmss') + 'Z'
        );

        req.setHeader('Content-Length', String.valueOf(httpBody.length()));

        req.setHeader(
            'Authorization',
            'AWS4-HMAC-SHA256 Credential=' + this.AWSAccessKeyId + '/' + credentialScope + ',' +
            'SignedHeaders=' + String.join(Utility.getLowerCaseSortedList(signedHeaders), ';') + ',' + 
            'Signature=' + signature
        );
        req.setEndpoint('https://' + hostname + resource);
        req.setMethod('POST');
        req.setBody(httpBody);

        Http http = new Http();
        HTTPResponse res = http.send(req);
        system.debug(res.getHeader('Location'));
        return res.getBody();
    }

    /**
     * HashedCanonicalRequestを作成
     */
    private String createHashedCanonicalRequest(
        String algorithm,
        String method,
        String url,
        Map<String, String> params,
        Map<String, String> headers,
        List<String> signedHeaders,
        String payload
    ) {     
        String signature = '';
        signature += method + Constants.LF;
        signature += url + Constants.LF;
        signature += Utility.getSortedParam(params) + Constants.LF;
        signature += Utility.createCanonicalHeaders(headers) + Constants.LF;
        signature += String.join(Utility.getLowerCaseSortedList(signedHeaders), ';').toLowerCase() + Constants.LF;
        signature += EncodingUtil.convertToHex(
                        Crypto.generateDigest(
                            algorithm,
                            Blob.valueOf(payload)
                        )
                     );
        return EncodingUtil.convertToHex(
            Crypto.generateDigest(
                algorithm, 
                Blob.valueOf(signature)
            )
        ).toLowerCase();
    }

    /**
     * StringToSignを作成
     */
    private String createStringToSign(
        String algorithm,
        DateTime requestDateTime,
        String credentialScope,
        String hashedCanonicalRequest
    ) {
        String stringToSign = algorithm + Constants.LF;
        stringToSign += 
           requestDateTime.formatGmt('YYYYMMdd') + 'T' + 
           requestDateTime.formatGmt('HHmmss') + 'Z' + Constants.LF;
        stringToSign += credentialScope + Constants.LF;
        stringToSign += hashedCanonicalRequest;
        return stringToSign;        
    }

    private String createSignature(
        String algorithm,
        DateTime requestDateTime,
        String region,
        String service,
        Blob stringToSign
    ) {
        Blob kDate = Crypto.generateMac(
            algorithm,
            Blob.valueOf(requestDateTime.formatGmt('YYYYMMdd')),
            Blob.valueOf('AWS4' + this.AWSAccessKeySecret)
        );
        Blob kRegion = Crypto.generateMac(
            algorithm,
            Blob.valueOf(region),
            kDate
        );
        Blob kService = Crypto.generateMac(
            algorithm,
            Blob.valueOf(service),
            kRegion
        );

        Blob kSigning = Crypto.generateMac(
            algorithm,
            Blob.valueOf('aws4_request'),
            kService
        );

        return EncodingUtil.convertToHex(
            Crypto.generateMac(
                algorithm,
                stringToSign,
                kSigning
            )
        );
    }
}

詳しいAPI仕様はこちらから。

STSは基本的にSignature Version4で、AssumeRoleWithSAMLだけは認証情報が必要ありません。つまり、単純なHTTP-POST or HTTP-GETでOKです。Console画面への自動ログインURLも1回HTTP-GETでSigninToken取ってきて、それをGETパラメータに含めてURL生成するだけで超簡単。

ただし、AssumeRoleWithSAMLをするときにはRoleの設定に注意してください。詳しくは過去の記事参照。

このエントリーをはてなブックマークに追加