This repository has been archived by the owner on Nov 29, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 481
/
AbstractProfileBase.java
325 lines (285 loc) · 13.4 KB
/
AbstractProfileBase.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
/*
* Copyright 2009 Vladimir Schaefer
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.saml.websso;
import org.joda.time.DateTime;
import org.opensaml.Configuration;
import org.opensaml.common.SAMLException;
import org.opensaml.common.SAMLObjectBuilder;
import org.opensaml.common.SAMLVersion;
import org.opensaml.common.binding.artifact.SAMLArtifactMap;
import org.opensaml.common.binding.decoding.BasicURLComparator;
import org.opensaml.common.binding.decoding.URIComparator;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml2.core.*;
import org.opensaml.saml2.metadata.Endpoint;
import org.opensaml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.security.MetadataCriteria;
import org.opensaml.security.SAMLSignatureProfileValidator;
import org.opensaml.ws.message.encoder.MessageEncodingException;
import org.opensaml.xml.XMLObjectBuilderFactory;
import org.opensaml.xml.security.CriteriaSet;
import org.opensaml.xml.security.credential.UsageType;
import org.opensaml.xml.security.criteria.EntityIDCriteria;
import org.opensaml.xml.security.criteria.UsageCriteria;
import org.opensaml.xml.signature.Signature;
import org.opensaml.xml.signature.SignatureTrustEngine;
import org.opensaml.xml.validation.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.saml.context.SAMLMessageContext;
import org.springframework.security.saml.metadata.MetadataManager;
import org.springframework.security.saml.processor.SAMLProcessor;
import org.springframework.security.saml.util.SAMLUtil;
import org.springframework.util.Assert;
import java.util.Random;
/**
* Base superclass for classes implementing processing of SAML messages.
*
* @author Vladimir Schaefer
*/
public abstract class AbstractProfileBase implements InitializingBean {
/**
* Maximum time from response creation when the message is deemed valid.
*/
private int responseSkew = 60;
/**
* Maximum time between assertion creation and current time when the assertion is usable
*/
private int maxAssertionTime = 3000;
/**
* Class logger.
*/
protected final Logger log = LoggerFactory.getLogger(getClass());
protected MetadataManager metadata;
protected SAMLProcessor processor;
protected SAMLArtifactMap artifactMap;
protected XMLObjectBuilderFactory builderFactory;
protected URIComparator uriComparator;
public AbstractProfileBase() {
this.builderFactory = Configuration.getBuilderFactory();
this.uriComparator = new BasicURLComparator();
}
public AbstractProfileBase(SAMLProcessor processor, MetadataManager manager) {
this();
this.processor = processor;
this.metadata = manager;
}
/**
* Implementation are expected to provide an unique identifier for the profile this class implements.
*
* @return profile name
*/
public abstract String getProfileIdentifier();
/**
* Sets maximum difference between local time and time of the assertion creation which still allows
* message to be processed. Basically determines maximum difference between clocks of the IDP and SP machines.
* Defaults to 60.
*
* @param responseSkew response skew time (in seconds)
*/
public void setResponseSkew(int responseSkew) {
this.responseSkew = responseSkew;
}
/**
* @return response skew time (in seconds)
*/
public int getResponseSkew() {
return responseSkew;
}
/**
* Maximum time between assertion creation and current time when the assertion is usable in seconds.
*
* @return max assertion time
*/
public int getMaxAssertionTime() {
return maxAssertionTime;
}
/**
* Customizes max assertion time between assertion creation and it's usability. Default to 3000 seconds.
*
* @param maxAssertionTime time in seconds
*/
public void setMaxAssertionTime(int maxAssertionTime) {
this.maxAssertionTime = maxAssertionTime;
}
/**
* Method calls the processor and sends the message contained in the context. Subclasses can provide additional
* processing before the message delivery. Message is sent using binding defined in the peer entity of the context.
*
* @param context context
* @param sign whether the message should be signed
* @throws MetadataProviderException metadata error
* @throws SAMLException SAML encoding error
* @throws org.opensaml.ws.message.encoder.MessageEncodingException
* message encoding error
*/
protected void sendMessage(SAMLMessageContext context, boolean sign) throws MetadataProviderException, SAMLException, MessageEncodingException {
processor.sendMessage(context, sign);
}
/**
* Method calls the processor and sends the message contained in the context. Subclasses can provide additional
* processing before the message delivery. Message is sent using the specified binding.
*
* @param context context
* @param sign whether the message should be signed
* @param binding binding to use to send the message
* @throws MetadataProviderException metadata error
* @throws SAMLException SAML encoding error
* @throws org.opensaml.ws.message.encoder.MessageEncodingException
* message encoding error
*/
protected void sendMessage(SAMLMessageContext context, boolean sign, String binding) throws MetadataProviderException, SAMLException, MessageEncodingException {
processor.sendMessage(context, sign, binding);
}
protected Status getStatus(String code, String statusMessage) {
SAMLObjectBuilder<StatusCode> codeBuilder = (SAMLObjectBuilder<StatusCode>) builderFactory.getBuilder(StatusCode.DEFAULT_ELEMENT_NAME);
StatusCode statusCode = codeBuilder.buildObject();
statusCode.setValue(code);
SAMLObjectBuilder<Status> statusBuilder = (SAMLObjectBuilder<Status>) builderFactory.getBuilder(Status.DEFAULT_ELEMENT_NAME);
Status status = statusBuilder.buildObject();
status.setStatusCode(statusCode);
if (statusMessage != null) {
SAMLObjectBuilder<StatusMessage> messageBuilder = (SAMLObjectBuilder<StatusMessage>) builderFactory.getBuilder(StatusMessage.DEFAULT_ELEMENT_NAME);
StatusMessage statusMessageObject = messageBuilder.buildObject();
statusMessageObject.setMessage(statusMessage);
status.setStatusMessage(statusMessageObject);
}
return status;
}
/**
* Fills the request with version, issue instants and destination data.
*
* @param localEntityId entityId of the local party acting as message issuer
* @param request request to be filled
* @param service service to use as destination for the request
*/
protected void buildCommonAttributes(String localEntityId, RequestAbstractType request, Endpoint service) {
request.setID(generateID());
request.setIssuer(getIssuer(localEntityId));
request.setVersion(SAMLVersion.VERSION_20);
request.setIssueInstant(new DateTime());
if (service != null) {
// Service is now known when we do not know which IDP will be used
request.setDestination(service.getLocation());
}
}
protected Issuer getIssuer(String localEntityId) {
SAMLObjectBuilder<Issuer> issuerBuilder = (SAMLObjectBuilder<Issuer>) builderFactory.getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
Issuer issuer = issuerBuilder.buildObject();
issuer.setValue(localEntityId);
return issuer;
}
/**
* Generates random ID to be used as Request/Response ID.
*
* @return random ID
*/
protected String generateID() {
Random r = new Random();
return 'a' + Long.toString(Math.abs(r.nextLong()), 20) + Long.toString(Math.abs(r.nextLong()), 20);
}
protected void verifyIssuer(Issuer issuer, SAMLMessageContext context) throws SAMLException {
// Validate format of issuer
if (issuer.getFormat() != null && !issuer.getFormat().equals(NameIDType.ENTITY)) {
throw new SAMLException("Issuer invalidated by issuer type " + issuer.getFormat());
}
// Validate that issuer is expected peer entity
if (!context.getPeerEntityMetadata().getEntityID().equals(issuer.getValue())) {
throw new SAMLException("Issuer invalidated by issuer value " + issuer.getValue());
}
}
/**
* Verifies that the destination URL intended in the message matches with the endpoint address. The URL message
* was ultimately received doesn't need to necessarily match the one defined in the metadata (in case of e.g. reverse-proxying
* of messages).
*
* @param endpoint endpoint the message was received at
* @param destination URL of the endpoint the message was intended to be sent to by the peer or null when not included
* @throws SAMLException in case endpoint doesn't match
*/
protected void verifyEndpoint(Endpoint endpoint, String destination) throws SAMLException {
// Verify that destination in the response matches one of the available endpoints
if (destination != null) {
if (uriComparator.compare(destination, endpoint.getLocation())) {
// Expected
} else if (uriComparator.compare(destination, endpoint.getResponseLocation())) {
// Expected
} else {
throw new SAMLException("Intended destination " + destination + " doesn't match any of the endpoint URLs on endpoint " + endpoint.getLocation() + " for profile " + getProfileIdentifier());
}
}
}
protected void verifySignature(Signature signature, String IDPEntityID, SignatureTrustEngine trustEngine) throws org.opensaml.xml.security.SecurityException, ValidationException {
if (trustEngine == null) {
throw new SecurityException("Trust engine is not set, signature can't be verified");
}
SAMLSignatureProfileValidator validator = new SAMLSignatureProfileValidator();
validator.validate(signature);
CriteriaSet criteriaSet = new CriteriaSet();
criteriaSet.add(new EntityIDCriteria(IDPEntityID));
criteriaSet.add(new MetadataCriteria(IDPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS));
criteriaSet.add(new UsageCriteria(UsageType.SIGNING));
log.debug("Verifying signature", signature);
if (!trustEngine.validate(signature, criteriaSet)) {
throw new ValidationException("Signature is not trusted or invalid");
}
}
/**
* Method is expected to return binding used to transfer messages to this endpoint. For some profiles the
* binding attribute in the metadata contains the profile name, method correctly parses the real binding
* in these situations.
*
* @param endpoint endpoint
* @return binding
*/
protected String getEndpointBinding(Endpoint endpoint) {
return SAMLUtil.getBindingForEndpoint(endpoint);
}
/**
* Determines whether given endpoint can be used together with the specified binding.
* <p>
* By default value of the binding in the endpoint is compared for equality with the user provided binding.
* <p>
* Method is automatically called for verification of user supplied binding value in the WebSSOProfileOptions.
*
* @param endpoint endpoint to check
* @param binding binding the endpoint must support for the method to return true
* @return true if given endpoint can be used with the binding
*/
protected boolean isEndpointMatching(Endpoint endpoint, String binding) {
return binding.equals(getEndpointBinding(endpoint));
}
@Autowired
public void setMetadata(MetadataManager metadata) {
this.metadata = metadata;
}
@Autowired(required = false)
public void setProcessor(SAMLProcessor processor) {
this.processor = processor;
}
// TODO autowire when ready
public void setArtifactMap(SAMLArtifactMap artifactMap) {
this.artifactMap = artifactMap;
}
public void afterPropertiesSet() throws Exception {
// TODO verify artifact map when ready
Assert.notNull(metadata, "Metadata must be set");
Assert.notNull(processor, "SAML Processor must be set");
}
}