目次

SAML Security token 取得の実装

ここでは、POC of consuming Sharepoint onlineのPOC段階2について、Javaの実装例を紹介します。

動作環境について

テスト環境についてですが、Java7(IBM J9 VM (build 2.6, JRE 1.7.0 Windows 7 amd64-64 Compressed References 20150701_255667 (JIT enabled, AOT enabled))で、テストを行いました。
ビルドには、Mavenを使用しました。
必要なlibrary dependencyを以下に示します。

  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>4.3.25.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.3</version>
  </dependency>
  <dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
  </dependency>
  <dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
    <version>20210307</version>
  </dependency>
  <dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bctls-fips</artifactId>
    <version>1.0.12.1</version>
    <scope>provided</scope>
  </dependency>

httpclientとして、apacheのhttpclientを使用していますが、Okhttpを使っても構いません。
また、RestTemplateを使用するため、spring-webを導入しています。
特に注目して欲しい のは、boucycastleライブラリです。bouncycastleライブラリを導入する理由については、Binary token取得のjava実装編で紹介します。

Main class code

実装のメインになるコードを以下に示します。

  private static final ResourceBundle RSC = ResourceBundle.getBundle("com.app.sample.ws.application");
  private static final String STS_AUTH_ENDPOINT = "https://xxxxx.contoso.com/adfs/services/trust/2005/usernamemixed";
 
  private Map<String, String> namespacePrefixes = new HashMap<String, String>();
 
  static {
    //register the prefix of NameSpace
    namespacePrefixes.put("t", "http://schemas.xmlsoap.org/ws/2005/02/trust");
  }
 
  public String receiveSamlSecurityToken() {
    String _token = "";
    try {
      //request entity
      RequestEntity<String> _requestEntity = RequestEntity
	     .post(new URI(STS_AUTH_ENDPOINT))
	     .header("content-type", "application/soap+xml; charset=utf-8")
	     .body(buildSamlSecurityRequestEnvelope()); ★Point1
      RestTemplate _restTemplate = new RestTemplate();
      _restTemplate.setRequestFactory(buildHttpComponentsClientHttpRequestFactory()); ★Point2
 
      ResponseEntity<String> _responseEntity = _restTemplate.exchange(_requestEntity, String.class);
      DOMResult _result = new DOMResult();
 
      Transformer _transformer = TransformerFactory.newInstance().newTransformer();
      _transformer.transform(new StringSource(_responseEntity.getBody()), _result);
 
      Document _definitionDocument = (Document) _result.getNode();
      final String XPATH_EXPRESSION = "//t:RequestedSecurityToken/*";   ★Point3
      Node _tokenNode = getXPathExpression(XPATH_EXPRESSION).evaluateAsNode(_definitionDocument);
      _token = nodeToXmlString(_tokenNode);   ★Point4
 
      if ("".equals(_token)) {
        logger.error("Unable to authenticate: empty token");
      }
 
    } catch (Exception e) {
      logger.error("failed to receive SAML security token", e);
    }
    return _token;
  }//receiveSamlSecurityToken
 
  //build SAML request envelope
  private String buildSamlSecurityRequestEnvelope() {
    //UserName token mapping
    Map<String, String> _mapRequest = new HashMap<String, String>();
    _mapRequest.put("sts_auth_url", STS_AUTH_ENDPOINT);
    _mapRequest.put("loginuser", RSC.getString("soap.auth.username"));
    _mapRequest.put("loginpass", RSC.getString("soap.auth.password"));
    //replace placeHolder
    StringSubstitutor _substitutor = new StringSubstitutor(_mapRequest, "%(", ")");
    String _finalXMLRequest = _substitutor.replace(RSC.getString("soap.saml.token.request"));
 
    return _finalXMLRequest;
  }//buildSamlSecurityRequestEnvelope
 
  private HttpComponentsClientHttpRequestFactory buildHttpComponentsClientHttpRequestFactory() throws Exception {
    return HttpComponentsClientHttpRequestFactoryBuilder.build();
  }
 
  //create xPathExpression
  private XPathExpression getXPathExpression(String expression) {
    XPathExpression _xPathExpressioin = XPathExpressionFactory.createXPathExpression(expression, namespacePrefixes);
    return _xPathExpressioin;
  }
 
  //convert nodes to XML string
  private String nodeToXmlString(Node node) throws Exception {
    StringWriter _writer = new StringWriter();
    Transformer _transformer = TransformerFactory.newInstance().newTransformer();
    _transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
    _transformer.setOutputProperty(OutputKeys.INDENT, "no");
    _transformer.transform(new DOMSource(node), new StreamResult(_writer));
 
    return _writer.toString();
  }

★Point1
SAML requestメッセージを作成する部分になります。
SOAP Envelopeメッセージ(xml)は、application.propertiesに格納されています。
application.propertiesの中身を以下に示します。

soap.auth.username=Sharepointユーザーアカウント
soap.auth.password=ユーザーパスワード
soap.saml.token.request=<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"><s:Header><a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action><a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo><a:To s:mustUnderstand="1">%(sts_auth_url)</a:To><o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><o:UsernameToken><o:Username>%(loginuser)</o:Username><o:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">%(loginpass)</o:Password></o:UsernameToken></o:Security></s:Header><s:Body><t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"><wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"><a:EndpointReference><a:Address>urn:federation:MicrosoftOnline</a:Address></a:EndpointReference></wsp:AppliesTo><t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType><t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType><t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType></t:RequestSecurityToken></s:Body></s:Envelope>

★Point2
HttpComponentsClientHttpRequestFactoryを作成する部分です。
ごく一般的設定になりますが、ポイントとしてUser-Agentの指定です。1)
コードの一部分を以下に示します。

public class HttpComponentsClientHttpRequestFactoryBuilder {
  public static HttpComponentsClientHttpRequestFactory build() {
    final int TIMEOUT = 5;
    //create connection manager
    PoolingHttpClientConnectionManager _cm = new PoolingHttpClientConnectionManager();
    _cm.setMaxTotal(128); //default 20
    _cm.setDefaultMaxPerRoute(24); //default 2
 
    //request configuration
    RequestConfig _requestConfig = RequestConfig.custom()
	.setConnectTimeout(TIMEOUT * 1000)
	.setConnectionRequestTimeout(TIMEOUT * 1000)
        .setSocketTimeout(TIMEOUT * 1000)
	.build();
 
    //create HttpClient
    HttpClientBuilder _builder = HttpClientBuilder.create()
	.setUserAgent("NONISV|Contoso|Sharepoint-demo/1.0")
	.setConnectionManager(_cm)
	.setDefaultRequestConfig(_requestConfig);
 
    HttpComponentsClientHttpRequestFactory _factory = new HttpComponentsClientHttpRequestFactory(_builder.build());
 
    return _factory;
  }
 
}

★Point3
SAML:Assertionを取り出す部分です。 ResponseのXMLから<t:RequestedSecurityToken>elementをXPathを利用して取り出します。
elementがnamespaceのものならXPathExpression生成時、namespaceを渡す必要があります。

★Point4
取り出したSAML:Assertionは、Node(Pretty-type)です。POCでも触れましたが、Binary tokenを取得する際、渡すSAML:Assertionはraw-typeです。
そのため、ここでNodeをStringに変換しています。

1)
User-Agentを指定する理由は、SharePointサイトからの調整を回避するためです。詳細は調整を回避するために、http トラフィックを装飾する方法を参照してください。