OpenSAML 3 で AuthnRequest を 生成する

(毎日更新すると約束したな。アレは嘘だ。ごめんなさい)

概要

自前で SAML 認証用の ServiceProvider (SP) を作っている。

Java で書いてるので Shibboleth 社のOSSライブラリである OpenSAML を利用しているが、 7月末でOpenSAML 2 系が EOL になるため、v3 系に移行しようと頑張っている。

問題点

v2 系では以下のように AuthnRequest の生成を実装していた。

DefaultBootstrap.bootstrap();

IssuerBuilder issuerBuilder = new IssuerBuilder();
Issuer issuer = issuerBuilder.buildObject(SAMLConstants.SAML20_NS, "Issuer", "samlp");
issuer.setValue("http://hogehoge.hoge");

NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder();
NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject();
nameIdPolicy.setFormat("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified");

AuthnRequestBuilder authnRequestBuilder = new AuthnRequestBuilder();
AuthnRequest authnRequest = authnRequestBuilder.buildObject("urn:oasis:names:tc:SAML:2.0:protocol", "AuthnRequest", "samlp");
authnRequest.setIssuer(issuer);
authnRequest.setNameIDPolicy(nameIdPolicy);
authnRequest.setAssertionConsumerServiceURL("http://hogehoge.hogehoge/acs");
authnRequest.setIssueInstant(new DateTime());
authnRequest.setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
authnRequest.setVersion(SAMLVersion.VERSION_20);
authnRequest.setID("ctestid890123456789012345678901234567890");

Marshaller marshaller = Configuration.getMarshallerFactory().getMarshaller(authnRequest);
Element authDOM = marshaller.marshall(authnRequest);

DOMImplementationLS domImplLS = (DOMImplementationLS) authDOM.getOwnerDocument().getImplementation();
LSSerializer serializer = domImplLS.createLSSerializer();
serializer.getDomConfig().setParameter("xml-declaration", false);

String messageXml = serializer.writeToString(authDOM);

Deflater deflater = new Deflater(Deflater.DEFLATED, true);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DeflaterOutputStream deflaterOutputStream =
new DeflaterOutputStream(byteArrayOutputStream, deflater);
deflaterOutputStream.write(messageXml.getBytes());
deflaterOutputStream.close();

String samlRequest = new String(Base64.encodeBase64(byteArrayOutputStream.toByteArray(), false));
samlRequest = URLEncoder.encode(samlRequest, "UTF-8");

ひとまずこのソースのまま OpenSAML 2.6.4 から OpenSAML 3.1.1 に置き換えたところ、

  • DefaultBootstrap.bootstrap();
  • Marshaller marshaller = Configuration.getMarshallerFactory().getMarshaller(authnRequest);

の2箇所でエラーが出てしまった。

解決策

DefaultBootstrap.bootstrap();

v2 系では DefaultBootstrap.bootstrap(); のようにライブラリの初期化を行っていたが、v3 系ではそれとは異なり、 InitializationService.initialize(); のように初期化を行うようだ。

Initialization and Configuration - OpenSAML 3 - Confluence

Various components of the OpenSAML library must be initialized before they can be used. Since the library is in itself modular, the initialization process takes a modular approach use the Java Service API.
The 2 primary components with which to be familiar are:

  • org.opensaml.core.config.Initializer
  • org.opensaml.core.config.InitializationService

Configuration.getMarshallerFactory()〜

Configuration クラスが存在しないため、エラーが出ていた。

上記の DefaultBootstrap のこともあり、どうも初期化の扱いがいろいろ変わっているようで、インスタンスをどこから取得すればいいのかなーと調べていたところ、Shibboleth 社の Developers Forum で以下のトピックを発見した。

shibboleth.1660669.n2.nabble.com

トピック内でやりとりされている通り、InitializationService#initialize() で初期化された各種インスタンスXMLObjectProviderRegistrySupport クラスから直接取得することができるようで、XMLObjectProviderRegistrySupport#getMarshallerFactory() を利用することで無事取得することができた。

(単にMarshallerFactory クラスを new してもからっぽのインスタンスができるだけ、だそうです。)
(また Unmarshaller の場合も同様のようです。)

結論

上記をふまえ、以下のように実装することで解決。

IssuerBuilder issuerBuilder = new IssuerBuilder();
Issuer issuer = issuerBuilder.buildObject(SAMLConstants.SAML20_NS, "Issuer", "samlp");
issuer.setValue("http://hogehoge.hoge");

NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder();
NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject();
nameIdPolicy.setFormat("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified");

AuthnRequestBuilder authnRequestBuilder = new AuthnRequestBuilder();
AuthnRequest authnRequest = authnRequestBuilder.buildObject("urn:oasis:names:tc:SAML:2.0:protocol", "AuthnRequest", "samlp");
authnRequest.setIssuer(issuer);
authnRequest.setNameIDPolicy(nameIdPolicy);
authnRequest.setAssertionConsumerServiceURL("http://hogehoge.hogehoge/acs");
authnRequest.setIssueInstant(new DateTime());
authnRequest.setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
authnRequest.setVersion(SAMLVersion.VERSION_20);
authnRequest.setID("ctestid890123456789012345678901234567890");

MarshallerFactory marshallerFactory = XMLObjectProviderRegistrySupport.getMarshallerFactory();
Marshaller marshaller = marshallerFactory.getMarshaller(authnRequest);
Element authDOM = marshaller.marshall(authnRequest);

DOMImplementationLS domImplLS = (DOMImplementationLS) authDOM.getOwnerDocument().getImplementation();
LSSerializer serializer = domImplLS.createLSSerializer();
serializer.getDomConfig().setParameter("xml-declaration", false);

String messageXml = serializer.writeToString(authDOM);

Deflater deflater = new Deflater(Deflater.DEFLATED, true);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DeflaterOutputStream deflaterOutputStream =
new DeflaterOutputStream(byteArrayOutputStream, deflater);
deflaterOutputStream.write(messageXml.getBytes());
deflaterOutputStream.close();

String samlRequest = new String(Base64.encodeBase64(byteArrayOutputStream.toByteArray(), false));
samlRequest = URLEncoder.encode(samlRequest, "UTF-8");