Saml 1.1 Validate support
The reasoning for this commit is that this is well supported by CAS,
and it might be reasonable to include this in something that emulates
CAS.
Use Saml lib provided by keycloak
6 files modified
7 files added
| | |
| | | <groupId>org.jboss.logging</groupId> |
| | | <artifactId>jboss-logging</artifactId> |
| | | <version>${jboss.logging.version}</version> |
| | | <scope>provided</scope> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.jboss.logging</groupId> |
| | |
| | | <groupId>org.keycloak</groupId> |
| | | <artifactId>keycloak-saml-core</artifactId> |
| | | <version>${keycloak.version}</version> |
| | | <scope>test</scope> |
| | | <scope>provided</scope> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>junit</groupId> |
| | |
| | | <plugin> |
| | | <groupId>org.apache.maven.plugins</groupId> |
| | | <artifactId>maven-compiler-plugin</artifactId> |
| | | <version>3.1</version> |
| | | <version>3.8.0</version> |
| | | <configuration> |
| | | <source>${maven.compiler.source}</source> |
| | | <target>${maven.compiler.target}</target> |
| | |
| | | <configuration> |
| | | <archive> |
| | | <manifestEntries> |
| | | <Dependencies>javax.xml.bind.api,org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services</Dependencies> |
| | | <Dependencies>javax.xml.bind.api,org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services,org.keycloak.keycloak-saml-core,org.keycloak.keycloak-saml-core-public</Dependencies> |
| | | </manifestEntries> |
| | | </archive> |
| | | </configuration> |
| | |
| | | public static final String LOGIN_PROTOCOL = "cas"; |
| | | |
| | | public static final String SERVICE_PARAM = "service"; |
| | | public static final String TARGET_PARAM = "TARGET"; |
| | | public static final String RENEW_PARAM = "renew"; |
| | | public static final String GATEWAY_PARAM = "gateway"; |
| | | public static final String TICKET_PARAM = "ticket"; |
| | |
| | | import org.keycloak.events.EventBuilder; |
| | | import org.keycloak.models.KeycloakSession; |
| | | import org.keycloak.models.RealmModel; |
| | | import org.keycloak.protocol.cas.endpoints.AuthorizationEndpoint; |
| | | import org.keycloak.protocol.cas.endpoints.LogoutEndpoint; |
| | | import org.keycloak.protocol.cas.endpoints.ServiceValidateEndpoint; |
| | | import org.keycloak.protocol.cas.endpoints.ValidateEndpoint; |
| | | import org.keycloak.protocol.cas.endpoints.*; |
| | | import org.keycloak.services.resources.RealmsResource; |
| | | |
| | | import javax.ws.rs.Path; |
| | |
| | | return endpoint; |
| | | } |
| | | |
| | | @Path("samlValidate") |
| | | public Object validateSaml11() { |
| | | SamlValidateEndpoint endpoint = new SamlValidateEndpoint(realm, event); |
| | | ResteasyProviderFactory.getInstance().injectProperties(endpoint); |
| | | return endpoint; |
| | | } |
| | | |
| | | @Path("serviceValidate") |
| | | public Object serviceValidate() { |
| | | ServiceValidateEndpoint endpoint = new ServiceValidateEndpoint(realm, event); |
New file |
| | |
| | | package org.keycloak.protocol.cas.endpoints; |
| | | |
| | | import org.jboss.logging.Logger; |
| | | import org.jboss.resteasy.spi.HttpRequest; |
| | | import org.keycloak.common.ClientConnection; |
| | | import org.keycloak.events.Details; |
| | | import org.keycloak.events.Errors; |
| | | import org.keycloak.events.EventBuilder; |
| | | import org.keycloak.models.*; |
| | | import org.keycloak.protocol.ProtocolMapper; |
| | | import org.keycloak.protocol.cas.CASLoginProtocol; |
| | | import org.keycloak.protocol.cas.mappers.CASAttributeMapper; |
| | | import org.keycloak.protocol.cas.representations.CASErrorCode; |
| | | import org.keycloak.protocol.cas.utils.CASValidationException; |
| | | import org.keycloak.protocol.oidc.utils.RedirectUtils; |
| | | import org.keycloak.services.managers.AuthenticationManager; |
| | | import org.keycloak.services.managers.ClientSessionCode; |
| | | import org.keycloak.services.util.DefaultClientSessionContext; |
| | | |
| | | import javax.ws.rs.core.Context; |
| | | import javax.ws.rs.core.HttpHeaders; |
| | | import javax.ws.rs.core.Response; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | public abstract class AbstractValidateEndpoint { |
| | | protected final Logger logger = Logger.getLogger(getClass()); |
| | | @Context |
| | | protected KeycloakSession session; |
| | | @Context |
| | | protected ClientConnection clientConnection; |
| | | @Context |
| | | protected HttpRequest request; |
| | | @Context |
| | | protected HttpHeaders headers; |
| | | protected RealmModel realm; |
| | | protected EventBuilder event; |
| | | protected ClientModel client; |
| | | protected AuthenticatedClientSessionModel clientSession; |
| | | |
| | | public AbstractValidateEndpoint(RealmModel realm, EventBuilder event) { |
| | | this.realm = realm; |
| | | this.event = event; |
| | | } |
| | | |
| | | protected void checkSsl() { |
| | | if (!session.getContext().getUri().getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) { |
| | | throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "HTTPS required", Response.Status.FORBIDDEN); |
| | | } |
| | | } |
| | | |
| | | protected void checkRealm() { |
| | | if (!realm.isEnabled()) { |
| | | throw new CASValidationException(CASErrorCode.INTERNAL_ERROR, "Realm not enabled", Response.Status.FORBIDDEN); |
| | | } |
| | | } |
| | | |
| | | protected void checkClient(String service) { |
| | | if (service == null) { |
| | | event.error(Errors.INVALID_REQUEST); |
| | | throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.SERVICE_PARAM, Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | client = realm.getClients().stream() |
| | | .filter(c -> CASLoginProtocol.LOGIN_PROTOCOL.equals(c.getProtocol())) |
| | | .filter(c -> RedirectUtils.verifyRedirectUri(session.getContext().getUri(), service, realm, c) != null) |
| | | .findFirst().orElse(null); |
| | | if (client == null) { |
| | | event.error(Errors.CLIENT_NOT_FOUND); |
| | | throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Client not found", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | if (!client.isEnabled()) { |
| | | event.error(Errors.CLIENT_DISABLED); |
| | | throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Client disabled", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | event.client(client.getClientId()); |
| | | |
| | | session.getContext().setClient(client); |
| | | } |
| | | |
| | | protected void checkTicket(String ticket, boolean requireReauth) { |
| | | if (ticket == null) { |
| | | event.error(Errors.INVALID_CODE); |
| | | throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.TICKET_PARAM, Response.Status.BAD_REQUEST); |
| | | } |
| | | if (!ticket.startsWith(CASLoginProtocol.SERVICE_TICKET_PREFIX)) { |
| | | event.error(Errors.INVALID_CODE); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET_SPEC, "Malformed service ticket", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | String code = ticket.substring(CASLoginProtocol.SERVICE_TICKET_PREFIX.length()); |
| | | |
| | | String[] parts = code.split("\\."); |
| | | if (parts.length == 4) { |
| | | event.detail(Details.CODE_ID, parts[2]); |
| | | } |
| | | |
| | | ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, null, session, realm, client, event, AuthenticatedClientSessionModel.class); |
| | | if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) { |
| | | event.error(Errors.INVALID_CODE); |
| | | |
| | | // Attempt to use same code twice should invalidate existing clientSession |
| | | AuthenticatedClientSessionModel clientSession = parseResult.getClientSession(); |
| | | if (clientSession != null) { |
| | | clientSession.detachFromUserSession(); |
| | | } |
| | | |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code not valid", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | clientSession = parseResult.getClientSession(); |
| | | |
| | | if (parseResult.isExpiredToken()) { |
| | | event.error(Errors.EXPIRED_CODE); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code is expired", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | clientSession.setNote(CASLoginProtocol.SESSION_SERVICE_TICKET, ticket); |
| | | |
| | | if (requireReauth && AuthenticationManager.isSSOAuthentication(clientSession)) { |
| | | event.error(Errors.SESSION_EXPIRED); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Interactive authentication was requested but not performed", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | UserSessionModel userSession = clientSession.getUserSession(); |
| | | |
| | | if (userSession == null) { |
| | | event.error(Errors.USER_SESSION_NOT_FOUND); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User session not found", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | UserModel user = userSession.getUser(); |
| | | if (user == null) { |
| | | event.error(Errors.USER_NOT_FOUND); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User not found", Response.Status.BAD_REQUEST); |
| | | } |
| | | if (!user.isEnabled()) { |
| | | event.error(Errors.USER_DISABLED); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User disabled", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | event.user(userSession.getUser()); |
| | | event.session(userSession.getId()); |
| | | |
| | | if (!client.getClientId().equals(clientSession.getClient().getClientId())) { |
| | | event.error(Errors.INVALID_CODE); |
| | | throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Auth error", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | if (!AuthenticationManager.isSessionValid(realm, userSession)) { |
| | | event.error(Errors.USER_SESSION_NOT_FOUND); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Session not active", Response.Status.BAD_REQUEST); |
| | | } |
| | | } |
| | | |
| | | protected Map<String, Object> getUserAttributes() { |
| | | UserSessionModel userSession = clientSession.getUserSession(); |
| | | // CAS protocol does not support scopes, so pass null scopeParam |
| | | ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, null); |
| | | |
| | | Set<ProtocolMapperModel> mappings = clientSessionCtx.getProtocolMappers(); |
| | | KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); |
| | | Map<String, Object> attributes = new HashMap<>(); |
| | | for (ProtocolMapperModel mapping : mappings) { |
| | | ProtocolMapper mapper = (ProtocolMapper) sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); |
| | | if (mapper instanceof CASAttributeMapper) { |
| | | ((CASAttributeMapper) mapper).setAttribute(attributes, mapping, userSession, session, clientSessionCtx); |
| | | } |
| | | } |
| | | return attributes; |
| | | } |
| | | } |
New file |
| | |
| | | package org.keycloak.protocol.cas.endpoints; |
| | | |
| | | import org.keycloak.dom.saml.v1.protocol.SAML11ResponseType; |
| | | import org.keycloak.events.EventBuilder; |
| | | import org.keycloak.events.EventType; |
| | | import org.keycloak.models.RealmModel; |
| | | import org.keycloak.models.UserModel; |
| | | import org.keycloak.protocol.cas.CASLoginProtocol; |
| | | import org.keycloak.protocol.cas.representations.CASErrorCode; |
| | | import org.keycloak.protocol.cas.representations.SamlResponseHelper; |
| | | import org.keycloak.protocol.cas.utils.CASValidationException; |
| | | import org.keycloak.services.Urls; |
| | | import org.xml.sax.InputSource; |
| | | |
| | | import javax.ws.rs.Consumes; |
| | | import javax.ws.rs.POST; |
| | | import javax.ws.rs.Produces; |
| | | import javax.ws.rs.core.MultivaluedMap; |
| | | import javax.ws.rs.core.Response; |
| | | import javax.xml.namespace.NamespaceContext; |
| | | import javax.xml.xpath.XPath; |
| | | import javax.xml.xpath.XPathExpression; |
| | | import javax.xml.xpath.XPathExpressionException; |
| | | import javax.xml.xpath.XPathFactory; |
| | | import java.io.StringReader; |
| | | import java.util.*; |
| | | |
| | | import static org.keycloak.protocol.cas.CASLoginProtocol.TARGET_PARAM; |
| | | |
| | | public class SamlValidateEndpoint extends AbstractValidateEndpoint { |
| | | public SamlValidateEndpoint(RealmModel realm, EventBuilder event) { |
| | | super(realm, event.event(EventType.CODE_TO_TOKEN)); |
| | | } |
| | | |
| | | @POST |
| | | @Consumes("text/xml;charset=utf-8") |
| | | @Produces("text/xml;charset=utf-8") |
| | | public Response validate(String input) { |
| | | MultivaluedMap<String, String> queryParams = request.getUri().getQueryParameters(); |
| | | try { |
| | | String soapAction = Optional.ofNullable(request.getHttpHeaders().getHeaderString("SOAPAction")).map(s -> s.trim().replace("\"", "")).orElse(""); |
| | | if (!soapAction.equals("http://www.oasis-open.org/committees/security")) { |
| | | throw new CASValidationException(CASErrorCode.INTERNAL_ERROR, "Not a validation request", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | String service = queryParams.getFirst(TARGET_PARAM); |
| | | boolean renew = queryParams.containsKey(CASLoginProtocol.RENEW_PARAM); |
| | | |
| | | checkRealm(); |
| | | checkSsl(); |
| | | checkClient(service); |
| | | String issuer = Urls.realmIssuer(request.getUri().getBaseUri(), realm.getName()); |
| | | String ticket = getTicket(input); |
| | | |
| | | checkTicket(ticket, renew); |
| | | UserModel user = clientSession.getUserSession().getUser(); |
| | | |
| | | Map<String, Object> attributes = getUserAttributes(); |
| | | |
| | | SAML11ResponseType response = SamlResponseHelper.successResponse(issuer, user.getUsername(), attributes); |
| | | |
| | | return Response.ok(SamlResponseHelper.soap(response)).build(); |
| | | |
| | | } catch (CASValidationException ex) { |
| | | logger.warnf("Invalid SAML1.1 token %s", ex.getErrorDescription()); |
| | | |
| | | SAML11ResponseType response = SamlResponseHelper.errorResponse(ex); |
| | | return Response.ok().entity(SamlResponseHelper.soap(response)).build(); |
| | | } |
| | | } |
| | | |
| | | private String getTicket(String input) { |
| | | try { |
| | | XPath xPath = XPathFactory.newInstance().newXPath(); |
| | | xPath.setNamespaceContext(new MapNamespaceContext(Collections.singletonMap("samlp", "urn:oasis:names:tc:SAML:1.0:protocol"))); |
| | | |
| | | XPathExpression expression = xPath.compile("//samlp:AssertionArtifact/text()"); |
| | | |
| | | return expression.evaluate(new InputSource(new StringReader(input))); |
| | | } catch (XPathExpressionException ex) { |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, ex.getMessage(), Response.Status.BAD_REQUEST); |
| | | } |
| | | } |
| | | |
| | | private static class MapNamespaceContext implements NamespaceContext { |
| | | Map<String, String> map; |
| | | |
| | | private MapNamespaceContext(Map<String, String> map) { |
| | | this.map = map; |
| | | } |
| | | |
| | | @Override |
| | | public String getNamespaceURI(String s) { |
| | | return map.get(s); |
| | | } |
| | | |
| | | @Override |
| | | public String getPrefix(String s) { |
| | | return map.entrySet().stream().filter(e -> e.getValue().equals(s)).findFirst().map(Map.Entry::getKey).orElse(null); |
| | | } |
| | | |
| | | @Override |
| | | public Iterator<String> getPrefixes(String s) { |
| | | return map.keySet().iterator(); |
| | | } |
| | | } |
| | | } |
| | |
| | | @Override |
| | | protected Response successResponse() { |
| | | UserSessionModel userSession = clientSession.getUserSession(); |
| | | // CAS protocol does not support scopes, so pass null scopeParam |
| | | ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, null); |
| | | |
| | | Set<ProtocolMapperModel> mappings = clientSessionCtx.getProtocolMappers(); |
| | | KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); |
| | | Map<String, Object> attributes = new HashMap<>(); |
| | | for (ProtocolMapperModel mapping : mappings) { |
| | | ProtocolMapper mapper = (ProtocolMapper) sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); |
| | | if (mapper instanceof CASAttributeMapper) { |
| | | ((CASAttributeMapper) mapper).setAttribute(attributes, mapping, userSession, session, clientSessionCtx); |
| | | } |
| | | } |
| | | |
| | | Map<String, Object> attributes = getUserAttributes(); |
| | | CASServiceResponse serviceResponse = ServiceResponseHelper.createSuccess(userSession.getUser().getUsername(), attributes); |
| | | return prepare(Response.Status.OK, serviceResponse); |
| | | } |
| | |
| | | package org.keycloak.protocol.cas.endpoints; |
| | | |
| | | import org.jboss.logging.Logger; |
| | | import org.jboss.resteasy.annotations.cache.NoCache; |
| | | import org.jboss.resteasy.spi.HttpRequest; |
| | | import org.keycloak.common.ClientConnection; |
| | | import org.keycloak.events.Details; |
| | | import org.keycloak.events.Errors; |
| | | import org.keycloak.events.EventBuilder; |
| | | import org.keycloak.events.EventType; |
| | | import org.keycloak.models.*; |
| | | import org.keycloak.models.RealmModel; |
| | | import org.keycloak.protocol.cas.CASLoginProtocol; |
| | | import org.keycloak.protocol.cas.representations.CASErrorCode; |
| | | import org.keycloak.protocol.cas.utils.CASValidationException; |
| | | import org.keycloak.protocol.oidc.utils.RedirectUtils; |
| | | import org.keycloak.services.managers.AuthenticationManager; |
| | | import org.keycloak.services.managers.ClientSessionCode; |
| | | |
| | | import javax.ws.rs.GET; |
| | | import javax.ws.rs.core.*; |
| | | import javax.ws.rs.core.MediaType; |
| | | import javax.ws.rs.core.MultivaluedMap; |
| | | import javax.ws.rs.core.Response; |
| | | |
| | | public class ValidateEndpoint { |
| | | protected static final Logger logger = Logger.getLogger(ValidateEndpoint.class); |
| | | public class ValidateEndpoint extends AbstractValidateEndpoint { |
| | | |
| | | private static final String RESPONSE_OK = "yes\n"; |
| | | private static final String RESPONSE_FAILED = "no\n"; |
| | | |
| | | @Context |
| | | protected KeycloakSession session; |
| | | |
| | | @Context |
| | | protected ClientConnection clientConnection; |
| | | |
| | | @Context |
| | | protected HttpRequest request; |
| | | |
| | | @Context |
| | | protected HttpHeaders headers; |
| | | |
| | | protected RealmModel realm; |
| | | protected EventBuilder event; |
| | | protected ClientModel client; |
| | | protected AuthenticatedClientSessionModel clientSession; |
| | | |
| | | public ValidateEndpoint(RealmModel realm, EventBuilder event) { |
| | | this.realm = realm; |
| | | this.event = event; |
| | | super(realm, event); |
| | | } |
| | | |
| | | @GET |
| | |
| | | return Response.status(e.getStatus()).entity(RESPONSE_FAILED).type(MediaType.TEXT_PLAIN).build(); |
| | | } |
| | | |
| | | private void checkSsl() { |
| | | if (!session.getContext().getUri().getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) { |
| | | throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "HTTPS required", Response.Status.FORBIDDEN); |
| | | } |
| | | } |
| | | |
| | | private void checkRealm() { |
| | | if (!realm.isEnabled()) { |
| | | throw new CASValidationException(CASErrorCode.INTERNAL_ERROR, "Realm not enabled", Response.Status.FORBIDDEN); |
| | | } |
| | | } |
| | | |
| | | private void checkClient(String service) { |
| | | if (service == null) { |
| | | event.error(Errors.INVALID_REQUEST); |
| | | throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.SERVICE_PARAM, Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | client = realm.getClients().stream() |
| | | .filter(c -> CASLoginProtocol.LOGIN_PROTOCOL.equals(c.getProtocol())) |
| | | .filter(c -> RedirectUtils.verifyRedirectUri(session.getContext().getUri(), service, realm, c) != null) |
| | | .findFirst().orElse(null); |
| | | if (client == null) { |
| | | event.error(Errors.CLIENT_NOT_FOUND); |
| | | throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Client not found", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | if (!client.isEnabled()) { |
| | | event.error(Errors.CLIENT_DISABLED); |
| | | throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Client disabled", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | event.client(client.getClientId()); |
| | | |
| | | session.getContext().setClient(client); |
| | | } |
| | | |
| | | private void checkTicket(String ticket, boolean requireReauth) { |
| | | if (ticket == null) { |
| | | event.error(Errors.INVALID_CODE); |
| | | throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.TICKET_PARAM, Response.Status.BAD_REQUEST); |
| | | } |
| | | if (!ticket.startsWith(CASLoginProtocol.SERVICE_TICKET_PREFIX)) { |
| | | event.error(Errors.INVALID_CODE); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET_SPEC, "Malformed service ticket", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | String code = ticket.substring(CASLoginProtocol.SERVICE_TICKET_PREFIX.length()); |
| | | |
| | | String[] parts = code.split("\\."); |
| | | if (parts.length == 4) { |
| | | event.detail(Details.CODE_ID, parts[2]); |
| | | } |
| | | |
| | | ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, null, session, realm, client, event, AuthenticatedClientSessionModel.class); |
| | | if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) { |
| | | event.error(Errors.INVALID_CODE); |
| | | |
| | | // Attempt to use same code twice should invalidate existing clientSession |
| | | AuthenticatedClientSessionModel clientSession = parseResult.getClientSession(); |
| | | if (clientSession != null) { |
| | | clientSession.detachFromUserSession(); |
| | | } |
| | | |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code not valid", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | clientSession = parseResult.getClientSession(); |
| | | |
| | | if (parseResult.isExpiredToken()) { |
| | | event.error(Errors.EXPIRED_CODE); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code is expired", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | clientSession.setNote(CASLoginProtocol.SESSION_SERVICE_TICKET, ticket); |
| | | |
| | | if (requireReauth && AuthenticationManager.isSSOAuthentication(clientSession)) { |
| | | event.error(Errors.SESSION_EXPIRED); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Interactive authentication was requested but not performed", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | UserSessionModel userSession = clientSession.getUserSession(); |
| | | |
| | | if (userSession == null) { |
| | | event.error(Errors.USER_SESSION_NOT_FOUND); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User session not found", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | UserModel user = userSession.getUser(); |
| | | if (user == null) { |
| | | event.error(Errors.USER_NOT_FOUND); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User not found", Response.Status.BAD_REQUEST); |
| | | } |
| | | if (!user.isEnabled()) { |
| | | event.error(Errors.USER_DISABLED); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User disabled", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | event.user(userSession.getUser()); |
| | | event.session(userSession.getId()); |
| | | |
| | | if (!client.getClientId().equals(clientSession.getClient().getClientId())) { |
| | | event.error(Errors.INVALID_CODE); |
| | | throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Auth error", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | if (!AuthenticationManager.isSessionValid(realm, userSession)) { |
| | | event.error(Errors.USER_SESSION_NOT_FOUND); |
| | | throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Session not active", Response.Status.BAD_REQUEST); |
| | | } |
| | | |
| | | } |
| | | } |
New file |
| | |
| | | package org.keycloak.protocol.cas.representations; |
| | | |
| | | import org.keycloak.dom.saml.v1.assertion.*; |
| | | import org.keycloak.dom.saml.v1.protocol.SAML11ResponseType; |
| | | import org.keycloak.dom.saml.v1.protocol.SAML11StatusCodeType; |
| | | import org.keycloak.dom.saml.v1.protocol.SAML11StatusType; |
| | | import org.keycloak.protocol.cas.utils.CASValidationException; |
| | | import org.keycloak.saml.common.exceptions.ProcessingException; |
| | | import org.keycloak.saml.processing.core.saml.v1.SAML11Constants; |
| | | import org.keycloak.saml.processing.core.saml.v1.writers.SAML11ResponseWriter; |
| | | import org.keycloak.services.validation.Validation; |
| | | import org.w3c.dom.Document; |
| | | import org.w3c.dom.Element; |
| | | import org.w3c.dom.Node; |
| | | |
| | | import javax.xml.datatype.DatatypeConfigurationException; |
| | | import javax.xml.datatype.DatatypeFactory; |
| | | import javax.xml.datatype.XMLGregorianCalendar; |
| | | import javax.xml.namespace.QName; |
| | | import javax.xml.parsers.DocumentBuilderFactory; |
| | | import javax.xml.parsers.ParserConfigurationException; |
| | | import javax.xml.stream.XMLOutputFactory; |
| | | import javax.xml.stream.XMLStreamException; |
| | | import javax.xml.stream.XMLStreamWriter; |
| | | import javax.xml.transform.Transformer; |
| | | import javax.xml.transform.TransformerException; |
| | | import javax.xml.transform.TransformerFactory; |
| | | import javax.xml.transform.dom.DOMResult; |
| | | import javax.xml.transform.dom.DOMSource; |
| | | import javax.xml.transform.stream.StreamResult; |
| | | import java.io.StringWriter; |
| | | import java.net.URI; |
| | | import java.time.ZoneOffset; |
| | | import java.time.ZonedDateTime; |
| | | import java.util.*; |
| | | import java.util.function.Consumer; |
| | | import java.util.stream.Collectors; |
| | | import java.util.stream.Stream; |
| | | |
| | | public class SamlResponseHelper { |
| | | private final static DatatypeFactory factory; |
| | | |
| | | static { |
| | | try { |
| | | factory = DatatypeFactory.newInstance(); |
| | | } catch (DatatypeConfigurationException e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | public static SAML11ResponseType errorResponse(CASValidationException ex) { |
| | | ZonedDateTime nowZoned = ZonedDateTime.now(ZoneOffset.UTC); |
| | | XMLGregorianCalendar now = factory.newXMLGregorianCalendar(GregorianCalendar.from(nowZoned)); |
| | | |
| | | return applyTo(new SAML11ResponseType("_" + UUID.randomUUID().toString(), now), obj -> { |
| | | obj.setStatus(applyTo(new SAML11StatusType(), status -> { |
| | | status.setStatusCode(new SAML11StatusCodeType(QName.valueOf("samlp:RequestDenied"))); |
| | | status.setStatusMessage(ex.getErrorDescription()); |
| | | })); |
| | | }); |
| | | } |
| | | |
| | | public static SAML11ResponseType successResponse(String issuer, String username, Map<String, Object> attributes) { |
| | | ZonedDateTime nowZoned = ZonedDateTime.now(ZoneOffset.UTC); |
| | | XMLGregorianCalendar now = factory.newXMLGregorianCalendar(GregorianCalendar.from(nowZoned)); |
| | | |
| | | return applyTo(new SAML11ResponseType("_" + UUID.randomUUID().toString(), now), |
| | | obj -> { |
| | | obj.setStatus(applyTo(new SAML11StatusType(), status -> status.setStatusCode(SAML11StatusCodeType.SUCCESS))); |
| | | obj.add(applyTo(new SAML11AssertionType("_" + UUID.randomUUID().toString(), now), assertion -> { |
| | | assertion.setIssuer(issuer); |
| | | assertion.setConditions(applyTo(new SAML11ConditionsType(), conditions -> { |
| | | conditions.setNotBefore(now); |
| | | conditions.setNotOnOrAfter(factory.newXMLGregorianCalendar(GregorianCalendar.from(nowZoned.plusMinutes(5)))); |
| | | })); |
| | | assertion.add(applyTo(new SAML11AuthenticationStatementType( |
| | | URI.create(SAML11Constants.AUTH_METHOD_PASSWORD), |
| | | now |
| | | ), stmt -> stmt.setSubject(toSubject(username)))); |
| | | assertion.addAllStatements(toAttributes(username, attributes)); |
| | | })); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | private static List<SAML11StatementAbstractType> toAttributes(String username, Map<String, Object> attributes) { |
| | | List<SAML11AttributeType> converted = attributeElements(attributes); |
| | | if (converted.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return Collections.singletonList(applyTo( |
| | | new SAML11AttributeStatementType(), |
| | | attrs -> { |
| | | attrs.setSubject(toSubject(username)); |
| | | attrs.addAllAttributes(converted); |
| | | }) |
| | | ); |
| | | } |
| | | |
| | | private static List<SAML11AttributeType> attributeElements(Map<String, Object> attributes) { |
| | | return attributes.entrySet().stream().flatMap(e -> |
| | | toAttribute(e.getKey(), e.getValue()) |
| | | ).filter(a -> !a.get().isEmpty()).collect(Collectors.toList()); |
| | | } |
| | | |
| | | private static Stream<SAML11AttributeType> toAttribute(String name, Object value) { |
| | | if (name == null || value == null) { |
| | | return Stream.empty(); |
| | | } |
| | | |
| | | if (value instanceof Collection) { |
| | | return Stream.of(samlAttribute(name, listString((Collection<?>) value))); |
| | | } |
| | | return Stream.of(samlAttribute(name, Collections.singletonList(value.toString()))); |
| | | } |
| | | |
| | | private static SAML11AttributeType samlAttribute(String name, List<Object> listString) { |
| | | return applyTo( |
| | | new SAML11AttributeType(name, URI.create("http://www.ja-sig.org/products/cas/")), |
| | | attr -> attr.addAll(listString) |
| | | ); |
| | | } |
| | | |
| | | private static List<Object> listString(Collection<?> value) { |
| | | return value.stream().map(Object::toString).collect(Collectors.toList()); |
| | | } |
| | | |
| | | private static SAML11SubjectType toSubject(String username) { |
| | | return applyTo( |
| | | new SAML11SubjectType(), |
| | | subject -> subject.setChoice( |
| | | new SAML11SubjectType.SAML11SubjectTypeChoice( |
| | | applyTo( |
| | | new SAML11NameIdentifierType(username), |
| | | ctype -> ctype.setFormat(nameIdFormat(username)) |
| | | ) |
| | | ) |
| | | ) |
| | | ); |
| | | } |
| | | |
| | | private static URI nameIdFormat(String username) { |
| | | return URI.create(Validation.isEmailValid(username) ? |
| | | SAML11Constants.FORMAT_EMAIL_ADDRESS : |
| | | SAML11Constants.FORMAT_UNSPECIFIED |
| | | ); |
| | | } |
| | | |
| | | private static <A> A applyTo(A input, Consumer<A> setter) { |
| | | setter.accept(input); |
| | | return input; |
| | | } |
| | | |
| | | public static String soap(SAML11ResponseType response) { |
| | | try { |
| | | Document result = toDOM(response); |
| | | |
| | | Document doc = wrapSoap(result.getDocumentElement()); |
| | | return toString(doc); |
| | | |
| | | } catch (Exception e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | } |
| | | |
| | | public static Document toDOM(SAML11ResponseType response) throws ParserConfigurationException, XMLStreamException, ProcessingException { |
| | | DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); |
| | | dbf.setNamespaceAware(true); |
| | | |
| | | XMLOutputFactory factory = XMLOutputFactory.newFactory(); |
| | | |
| | | Document doc = dbf.newDocumentBuilder().newDocument(); |
| | | DOMResult result = new DOMResult(doc); |
| | | XMLStreamWriter xmlWriter = factory.createXMLStreamWriter(result); |
| | | SAML11ResponseWriter writer = new SAML11ResponseWriter(xmlWriter); |
| | | writer.write(response); |
| | | return doc; |
| | | } |
| | | |
| | | private static Document wrapSoap(Node node) throws ParserConfigurationException { |
| | | DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); |
| | | dbf.setNamespaceAware(true); |
| | | Document doc = dbf.newDocumentBuilder().newDocument(); |
| | | |
| | | Element envelope = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/", "soap:Envelope"); |
| | | envelope.appendChild(doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/", "soap:Header")); |
| | | Element body = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/", "soap:Body"); |
| | | |
| | | Node imported = doc.importNode(node, true); |
| | | |
| | | body.appendChild(imported); |
| | | doc.appendChild(body); |
| | | envelope.appendChild(body); |
| | | doc.appendChild(envelope); |
| | | return doc; |
| | | } |
| | | |
| | | public static String toString(Document document) throws TransformerException { |
| | | // Output the Document |
| | | TransformerFactory tf = TransformerFactory.newInstance(); |
| | | Transformer t = tf.newTransformer(); |
| | | DOMSource source = new DOMSource(document); |
| | | StringWriter writer = new StringWriter(); |
| | | StreamResult result = new StreamResult(writer); |
| | | t.transform(source, result); |
| | | return writer.toString(); |
| | | } |
| | | } |
New file |
| | |
| | | package org.keycloak.protocol.cas; |
| | | |
| | | import org.junit.Test; |
| | | import org.keycloak.dom.saml.v1.protocol.SAML11ResponseType; |
| | | import org.keycloak.protocol.cas.representations.CASErrorCode; |
| | | import org.keycloak.protocol.cas.representations.SamlResponseHelper; |
| | | import org.keycloak.protocol.cas.utils.CASValidationException; |
| | | import org.w3c.dom.Document; |
| | | |
| | | import javax.ws.rs.core.Response; |
| | | import java.util.Collections; |
| | | |
| | | import static org.junit.Assert.assertNotNull; |
| | | import static org.junit.Assert.assertTrue; |
| | | |
| | | public class SamlResponseTest { |
| | | @Test |
| | | public void successResponseIsWrappedInSOAP() { |
| | | SAML11ResponseType response = SamlResponseHelper.successResponse("keycloak", "test@example.com", Collections.emptyMap()); |
| | | String soapResult = SamlResponseHelper.soap(response); |
| | | assertTrue(soapResult.contains("samlp:Success")); |
| | | assertTrue(soapResult.contains("test@example.com")); |
| | | assertTrue(soapResult.contains("keycloak")); |
| | | } |
| | | |
| | | @Test |
| | | public void failureResponseIsWrappedInSOAP() { |
| | | SAML11ResponseType response = SamlResponseHelper.errorResponse(new CASValidationException(CASErrorCode.INVALID_TICKET, "Nope", Response.Status.BAD_REQUEST)); |
| | | String nope = SamlResponseHelper.soap(response); |
| | | assertTrue(nope.contains("Nope")); |
| | | } |
| | | |
| | | @Test |
| | | public void validateSchemaResponseFailure() throws Exception { |
| | | SAML11ResponseType response = SamlResponseHelper.errorResponse(new CASValidationException(CASErrorCode.INVALID_TICKET, "Nope", Response.Status.BAD_REQUEST)); |
| | | String output = SamlResponseHelper.toString(SamlResponseHelper.toDOM(response)); |
| | | Document doc = XMLValidator.parseAndValidate(output, XMLValidator.schemaFromClassPath("oasis-sstc-saml-schema-protocol-1.1.xsd")); |
| | | assertNotNull(doc); |
| | | } |
| | | |
| | | @Test |
| | | public void validateSchemaResponseSuccess() throws Exception { |
| | | SAML11ResponseType response = SamlResponseHelper.successResponse("keycloak", "test@example.com", Collections.emptyMap()); |
| | | String output = SamlResponseHelper.toString(SamlResponseHelper.toDOM(response)); |
| | | Document doc = XMLValidator.parseAndValidate(output, XMLValidator.schemaFromClassPath("oasis-sstc-saml-schema-protocol-1.1.xsd")); |
| | | assertNotNull(doc); |
| | | } |
| | | } |
| | |
| | | package org.keycloak.protocol.cas; |
| | | |
| | | import com.jayway.jsonpath.JsonPath; |
| | | import com.sun.xml.bind.v2.util.FatalAdapter; |
| | | import org.junit.Test; |
| | | import org.keycloak.protocol.cas.representations.CASErrorCode; |
| | | import org.keycloak.protocol.cas.representations.CASServiceResponse; |
| | |
| | | import org.keycloak.protocol.cas.utils.ServiceResponseMarshaller; |
| | | import org.w3c.dom.Document; |
| | | import org.w3c.dom.Node; |
| | | import org.xml.sax.helpers.DefaultHandler; |
| | | import org.xmlunit.xpath.JAXPXPathEngine; |
| | | import org.xmlunit.xpath.XPathEngine; |
| | | |
| | | import javax.xml.XMLConstants; |
| | | import javax.xml.parsers.DocumentBuilder; |
| | | import javax.xml.parsers.DocumentBuilderFactory; |
| | | import javax.xml.validation.Schema; |
| | | import javax.xml.validation.SchemaFactory; |
| | | import java.io.ByteArrayInputStream; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.util.*; |
| | | |
| | | import static org.junit.Assert.assertEquals; |
| | | import static org.keycloak.protocol.cas.XMLValidator.parseAndValidate; |
| | | import static org.keycloak.protocol.cas.XMLValidator.schemaFromClassPath; |
| | | |
| | | public class ServiceResponseTest { |
| | | private final XPathEngine xpath = new JAXPXPathEngine(); |
| | |
| | | // Build and validate XML response |
| | | |
| | | String xml = ServiceResponseMarshaller.marshalXml(response); |
| | | Document doc = parseAndValidate(xml); |
| | | Document doc = parseAndValidate(xml, schemaFromClassPath("cas-response-schema.xsd")); |
| | | assertEquals("username", xpath.evaluate("/cas:serviceResponse/cas:authenticationSuccess/cas:user", doc)); |
| | | int idx = 0; |
| | | for (Node node : xpath.selectNodes("/cas:serviceResponse/cas:authenticationSuccess/cas:attributes/cas:list", doc)) { |
| | |
| | | // Build and validate XML response |
| | | |
| | | String xml = ServiceResponseMarshaller.marshalXml(response); |
| | | Document doc = parseAndValidate(xml); |
| | | Document doc = parseAndValidate(xml, schemaFromClassPath("cas-response-schema.xsd")); |
| | | assertEquals(CASErrorCode.INVALID_REQUEST.name(), xpath.evaluate("/cas:serviceResponse/cas:authenticationFailure/@code", doc)); |
| | | assertEquals("Error description", xpath.evaluate("/cas:serviceResponse/cas:authenticationFailure", doc)); |
| | | } |
| | | |
| | | /** |
| | | * Parse XML document and validate against CAS schema |
| | | */ |
| | | private Document parseAndValidate(String xml) throws Exception { |
| | | Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) |
| | | .newSchema(getClass().getResource("cas-response-schema.xsd")); |
| | | |
| | | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| | | factory.setSchema(schema); |
| | | factory.setNamespaceAware(true); |
| | | DocumentBuilder builder = factory.newDocumentBuilder(); |
| | | builder.setErrorHandler(new FatalAdapter(new DefaultHandler())); |
| | | return builder.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))); |
| | | } |
| | | } |
New file |
| | |
| | | package org.keycloak.protocol.cas; |
| | | |
| | | import com.sun.xml.bind.v2.util.FatalAdapter; |
| | | import org.w3c.dom.Document; |
| | | import org.xml.sax.InputSource; |
| | | import org.xml.sax.SAXException; |
| | | import org.xml.sax.helpers.DefaultHandler; |
| | | |
| | | import javax.xml.XMLConstants; |
| | | import javax.xml.parsers.DocumentBuilder; |
| | | import javax.xml.parsers.DocumentBuilderFactory; |
| | | import javax.xml.validation.Schema; |
| | | import javax.xml.validation.SchemaFactory; |
| | | import java.io.StringReader; |
| | | |
| | | public abstract class XMLValidator { |
| | | private XMLValidator(){} |
| | | |
| | | public static Schema schemaFromClassPath(String path) throws SAXException { |
| | | return SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) |
| | | .newSchema(XMLValidator.class.getResource(path)); |
| | | } |
| | | |
| | | /** |
| | | * Parse XML document and validate against CAS schema |
| | | */ |
| | | public static Document parseAndValidate(String xml, Schema schema) throws Exception { |
| | | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| | | factory.setSchema(schema); |
| | | factory.setNamespaceAware(true); |
| | | DocumentBuilder builder = factory.newDocumentBuilder(); |
| | | builder.setErrorHandler(new FatalAdapter(new DefaultHandler())); |
| | | return builder.parse(new InputSource(new StringReader(xml))); |
| | | } |
| | | } |
New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <schema targetNamespace="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns="http://www.w3.org/2001/XMLSchema" elementFormDefault="unqualified" attributeFormDefault="unqualified" version="1.1"> |
| | | <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd"/> |
| | | <annotation> |
| | | <documentation> |
| | | Document identifier: oasis-sstc-saml-schema-assertion-1.1 |
| | | Location: http://www.oasis-open.org/committees/documents.php?wg_abbrev=security |
| | | Revision history: |
| | | V1.0 (November, 2002): |
| | | Initial standard schema. |
| | | V1.1 (September, 2003): |
| | | * Note that V1.1 of this schema has the same XML namespace as V1.0. |
| | | Rebased ID content directly on XML Schema types |
| | | Added DoNotCacheCondition element and DoNotCacheConditionType |
| | | </documentation> |
| | | </annotation> |
| | | <simpleType name="DecisionType"> |
| | | <restriction base="string"> |
| | | <enumeration value="Permit"/> |
| | | <enumeration value="Deny"/> |
| | | <enumeration value="Indeterminate"/> |
| | | </restriction> |
| | | </simpleType> |
| | | <element name="AssertionIDReference" type="NCName"/> |
| | | <element name="Assertion" type="saml:AssertionType"/> |
| | | <complexType name="AssertionType"> |
| | | <sequence> |
| | | <element ref="saml:Conditions" minOccurs="0"/> |
| | | <element ref="saml:Advice" minOccurs="0"/> |
| | | <choice maxOccurs="unbounded"> |
| | | <element ref="saml:Statement"/> |
| | | <element ref="saml:SubjectStatement"/> |
| | | <element ref="saml:AuthenticationStatement"/> |
| | | <element ref="saml:AuthorizationDecisionStatement"/> |
| | | <element ref="saml:AttributeStatement"/> |
| | | </choice> |
| | | <element ref="ds:Signature" minOccurs="0"/> |
| | | </sequence> |
| | | <attribute name="MajorVersion" type="integer" use="required"/> |
| | | <attribute name="MinorVersion" type="integer" use="required"/> |
| | | <attribute name="AssertionID" type="ID" use="required"/> |
| | | <attribute name="Issuer" type="string" use="required"/> |
| | | <attribute name="IssueInstant" type="dateTime" use="required"/> |
| | | </complexType> |
| | | <element name="Conditions" type="saml:ConditionsType"/> |
| | | <complexType name="ConditionsType"> |
| | | <choice minOccurs="0" maxOccurs="unbounded"> |
| | | <element ref="saml:AudienceRestrictionCondition"/> |
| | | <element ref="saml:DoNotCacheCondition"/> |
| | | <element ref="saml:Condition"/> |
| | | </choice> |
| | | <attribute name="NotBefore" type="dateTime" use="optional"/> |
| | | <attribute name="NotOnOrAfter" type="dateTime" use="optional"/> |
| | | </complexType> |
| | | <element name="Condition" type="saml:ConditionAbstractType"/> |
| | | <complexType name="ConditionAbstractType" abstract="true"/> |
| | | <element name="AudienceRestrictionCondition" type="saml:AudienceRestrictionConditionType"/> |
| | | <complexType name="AudienceRestrictionConditionType"> |
| | | <complexContent> |
| | | <extension base="saml:ConditionAbstractType"> |
| | | <sequence> |
| | | <element ref="saml:Audience" maxOccurs="unbounded"/> |
| | | </sequence> |
| | | </extension> |
| | | </complexContent> |
| | | </complexType> |
| | | <element name="Audience" type="anyURI"/> |
| | | <element name="DoNotCacheCondition" type="saml:DoNotCacheConditionType"/> |
| | | <complexType name="DoNotCacheConditionType"> |
| | | <complexContent> |
| | | <extension base="saml:ConditionAbstractType"/> |
| | | </complexContent> |
| | | </complexType> |
| | | <element name="Advice" type="saml:AdviceType"/> |
| | | <complexType name="AdviceType"> |
| | | <choice minOccurs="0" maxOccurs="unbounded"> |
| | | <element ref="saml:AssertionIDReference"/> |
| | | <element ref="saml:Assertion"/> |
| | | <any namespace="##other" processContents="lax"/> |
| | | </choice> |
| | | </complexType> |
| | | <element name="Statement" type="saml:StatementAbstractType"/> |
| | | <complexType name="StatementAbstractType" abstract="true"/> |
| | | <element name="SubjectStatement" type="saml:SubjectStatementAbstractType"/> |
| | | <complexType name="SubjectStatementAbstractType" abstract="true"> |
| | | <complexContent> |
| | | <extension base="saml:StatementAbstractType"> |
| | | <sequence> |
| | | <element ref="saml:Subject"/> |
| | | </sequence> |
| | | </extension> |
| | | </complexContent> |
| | | </complexType> |
| | | <element name="Subject" type="saml:SubjectType"/> |
| | | <complexType name="SubjectType"> |
| | | <choice> |
| | | <sequence> |
| | | <element ref="saml:NameIdentifier"/> |
| | | <element ref="saml:SubjectConfirmation" minOccurs="0"/> |
| | | </sequence> |
| | | <element ref="saml:SubjectConfirmation"/> |
| | | </choice> |
| | | </complexType> |
| | | <element name="NameIdentifier" type="saml:NameIdentifierType"/> |
| | | <complexType name="NameIdentifierType"> |
| | | <simpleContent> |
| | | <extension base="string"> |
| | | <attribute name="NameQualifier" type="string" use="optional"/> |
| | | <attribute name="Format" type="anyURI" use="optional"/> |
| | | </extension> |
| | | </simpleContent> |
| | | </complexType> |
| | | <element name="SubjectConfirmation" type="saml:SubjectConfirmationType"/> |
| | | <complexType name="SubjectConfirmationType"> |
| | | <sequence> |
| | | <element ref="saml:ConfirmationMethod" maxOccurs="unbounded"/> |
| | | <element ref="saml:SubjectConfirmationData" minOccurs="0"/> |
| | | <element ref="ds:KeyInfo" minOccurs="0"/> |
| | | </sequence> |
| | | </complexType> |
| | | <element name="SubjectConfirmationData" type="anyType"/> |
| | | <element name="ConfirmationMethod" type="anyURI"/> |
| | | <element name="AuthenticationStatement" type="saml:AuthenticationStatementType"/> |
| | | <complexType name="AuthenticationStatementType"> |
| | | <complexContent> |
| | | <extension base="saml:SubjectStatementAbstractType"> |
| | | <sequence> |
| | | <element ref="saml:SubjectLocality" minOccurs="0"/> |
| | | <element ref="saml:AuthorityBinding" minOccurs="0" maxOccurs="unbounded"/> |
| | | </sequence> |
| | | <attribute name="AuthenticationMethod" type="anyURI" use="required"/> |
| | | <attribute name="AuthenticationInstant" type="dateTime" use="required"/> |
| | | </extension> |
| | | </complexContent> |
| | | </complexType> |
| | | <element name="SubjectLocality" type="saml:SubjectLocalityType"/> |
| | | <complexType name="SubjectLocalityType"> |
| | | <attribute name="IPAddress" type="string" use="optional"/> |
| | | <attribute name="DNSAddress" type="string" use="optional"/> |
| | | </complexType> |
| | | <element name="AuthorityBinding" type="saml:AuthorityBindingType"/> |
| | | <complexType name="AuthorityBindingType"> |
| | | <attribute name="AuthorityKind" type="QName" use="required"/> |
| | | <attribute name="Location" type="anyURI" use="required"/> |
| | | <attribute name="Binding" type="anyURI" use="required"/> |
| | | </complexType> |
| | | <element name="AuthorizationDecisionStatement" type="saml:AuthorizationDecisionStatementType"/> |
| | | <complexType name="AuthorizationDecisionStatementType"> |
| | | <complexContent> |
| | | <extension base="saml:SubjectStatementAbstractType"> |
| | | <sequence> |
| | | <element ref="saml:Action" maxOccurs="unbounded"/> |
| | | <element ref="saml:Evidence" minOccurs="0"/> |
| | | </sequence> |
| | | <attribute name="Resource" type="anyURI" use="required"/> |
| | | <attribute name="Decision" type="saml:DecisionType" use="required"/> |
| | | </extension> |
| | | </complexContent> |
| | | </complexType> |
| | | <element name="Action" type="saml:ActionType"/> |
| | | <complexType name="ActionType"> |
| | | <simpleContent> |
| | | <extension base="string"> |
| | | <attribute name="Namespace" type="anyURI"/> |
| | | </extension> |
| | | </simpleContent> |
| | | </complexType> |
| | | <element name="Evidence" type="saml:EvidenceType"/> |
| | | <complexType name="EvidenceType"> |
| | | <choice maxOccurs="unbounded"> |
| | | <element ref="saml:AssertionIDReference"/> |
| | | <element ref="saml:Assertion"/> |
| | | </choice> |
| | | </complexType> |
| | | <element name="AttributeStatement" type="saml:AttributeStatementType"/> |
| | | <complexType name="AttributeStatementType"> |
| | | <complexContent> |
| | | <extension base="saml:SubjectStatementAbstractType"> |
| | | <sequence> |
| | | <element ref="saml:Attribute" maxOccurs="unbounded"/> |
| | | </sequence> |
| | | </extension> |
| | | </complexContent> |
| | | </complexType> |
| | | <element name="AttributeDesignator" type="saml:AttributeDesignatorType"/> |
| | | <complexType name="AttributeDesignatorType"> |
| | | <attribute name="AttributeName" type="string" use="required"/> |
| | | <attribute name="AttributeNamespace" type="anyURI" use="required"/> |
| | | </complexType> |
| | | <element name="Attribute" type="saml:AttributeType"/> |
| | | <complexType name="AttributeType"> |
| | | <complexContent> |
| | | <extension base="saml:AttributeDesignatorType"> |
| | | <sequence> |
| | | <element ref="saml:AttributeValue" maxOccurs="unbounded"/> |
| | | </sequence> |
| | | </extension> |
| | | </complexContent> |
| | | </complexType> |
| | | <element name="AttributeValue" type="anyType"/> |
| | | </schema> |
New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <schema targetNamespace="urn:oasis:names:tc:SAML:1.0:protocol" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" xmlns="http://www.w3.org/2001/XMLSchema" elementFormDefault="unqualified" attributeFormDefault="unqualified" version="1.1"> |
| | | <import namespace="urn:oasis:names:tc:SAML:1.0:assertion" schemaLocation="oasis-sstc-saml-schema-assertion-1.1.xsd"/> |
| | | <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd"/> |
| | | <annotation> |
| | | <documentation> |
| | | Document identifier: oasis-sstc-saml-schema-protocol-1.1 |
| | | Location: http://www.oasis-open.org/committees/documents.php?wg_abbrev=security |
| | | Revision history: |
| | | V1.0 (November, 2002): |
| | | Initial standard schema. |
| | | V1.1 (September, 2003): |
| | | * Note that V1.1 of this schema has the same XML namespace as V1.0. |
| | | Rebased ID content directly on XML Schema types |
| | | </documentation> |
| | | </annotation> |
| | | <complexType name="RequestAbstractType" abstract="true"> |
| | | <sequence> |
| | | <element ref="samlp:RespondWith" minOccurs="0" maxOccurs="unbounded"/> |
| | | <element ref="ds:Signature" minOccurs="0"/> |
| | | </sequence> |
| | | <attribute name="RequestID" type="ID" use="required"/> |
| | | <attribute name="MajorVersion" type="integer" use="required"/> |
| | | <attribute name="MinorVersion" type="integer" use="required"/> |
| | | <attribute name="IssueInstant" type="dateTime" use="required"/> |
| | | </complexType> |
| | | <element name="RespondWith" type="QName"/> |
| | | <element name="Request" type="samlp:RequestType"/> |
| | | <complexType name="RequestType"> |
| | | <complexContent> |
| | | <extension base="samlp:RequestAbstractType"> |
| | | <choice> |
| | | <element ref="samlp:Query"/> |
| | | <element ref="samlp:SubjectQuery"/> |
| | | <element ref="samlp:AuthenticationQuery"/> |
| | | <element ref="samlp:AttributeQuery"/> |
| | | <element ref="samlp:AuthorizationDecisionQuery"/> |
| | | <element ref="saml:AssertionIDReference" maxOccurs="unbounded"/> |
| | | <element ref="samlp:AssertionArtifact" maxOccurs="unbounded"/> |
| | | </choice> |
| | | </extension> |
| | | </complexContent> |
| | | </complexType> |
| | | <element name="AssertionArtifact" type="string"/> |
| | | <element name="Query" type="samlp:QueryAbstractType"/> |
| | | <complexType name="QueryAbstractType" abstract="true"/> |
| | | <element name="SubjectQuery" type="samlp:SubjectQueryAbstractType"/> |
| | | <complexType name="SubjectQueryAbstractType" abstract="true"> |
| | | <complexContent> |
| | | <extension base="samlp:QueryAbstractType"> |
| | | <sequence> |
| | | <element ref="saml:Subject"/> |
| | | </sequence> |
| | | </extension> |
| | | </complexContent> |
| | | </complexType> |
| | | <element name="AuthenticationQuery" type="samlp:AuthenticationQueryType"/> |
| | | <complexType name="AuthenticationQueryType"> |
| | | <complexContent> |
| | | <extension base="samlp:SubjectQueryAbstractType"> |
| | | <attribute name="AuthenticationMethod" type="anyURI"/> |
| | | </extension> |
| | | </complexContent> |
| | | </complexType> |
| | | <element name="AttributeQuery" type="samlp:AttributeQueryType"/> |
| | | <complexType name="AttributeQueryType"> |
| | | <complexContent> |
| | | <extension base="samlp:SubjectQueryAbstractType"> |
| | | <sequence> |
| | | <element ref="saml:AttributeDesignator" minOccurs="0" maxOccurs="unbounded"/> |
| | | </sequence> |
| | | <attribute name="Resource" type="anyURI" use="optional"/> |
| | | </extension> |
| | | </complexContent> |
| | | </complexType> |
| | | <element name="AuthorizationDecisionQuery" type="samlp:AuthorizationDecisionQueryType"/> |
| | | <complexType name="AuthorizationDecisionQueryType"> |
| | | <complexContent> |
| | | <extension base="samlp:SubjectQueryAbstractType"> |
| | | <sequence> |
| | | <element ref="saml:Action" maxOccurs="unbounded"/> |
| | | <element ref="saml:Evidence" minOccurs="0"/> |
| | | </sequence> |
| | | <attribute name="Resource" type="anyURI" use="required"/> |
| | | </extension> |
| | | </complexContent> |
| | | </complexType> |
| | | <complexType name="ResponseAbstractType" abstract="true"> |
| | | <sequence> |
| | | <element ref="ds:Signature" minOccurs="0"/> |
| | | </sequence> |
| | | <attribute name="ResponseID" type="ID" use="required"/> |
| | | <attribute name="InResponseTo" type="NCName" use="optional"/> |
| | | <attribute name="MajorVersion" type="integer" use="required"/> |
| | | <attribute name="MinorVersion" type="integer" use="required"/> |
| | | <attribute name="IssueInstant" type="dateTime" use="required"/> |
| | | <attribute name="Recipient" type="anyURI" use="optional"/> |
| | | </complexType> |
| | | <element name="Response" type="samlp:ResponseType"/> |
| | | <complexType name="ResponseType"> |
| | | <complexContent> |
| | | <extension base="samlp:ResponseAbstractType"> |
| | | <sequence> |
| | | <element ref="samlp:Status"/> |
| | | <element ref="saml:Assertion" minOccurs="0" maxOccurs="unbounded"/> |
| | | </sequence> |
| | | </extension> |
| | | </complexContent> |
| | | </complexType> |
| | | <element name="Status" type="samlp:StatusType"/> |
| | | <complexType name="StatusType"> |
| | | <sequence> |
| | | <element ref="samlp:StatusCode"/> |
| | | <element ref="samlp:StatusMessage" minOccurs="0"/> |
| | | <element ref="samlp:StatusDetail" minOccurs="0"/> |
| | | </sequence> |
| | | </complexType> |
| | | <element name="StatusCode" type="samlp:StatusCodeType"/> |
| | | <complexType name="StatusCodeType"> |
| | | <sequence> |
| | | <element ref="samlp:StatusCode" minOccurs="0"/> |
| | | </sequence> |
| | | <attribute name="Value" type="QName" use="required"/> |
| | | </complexType> |
| | | <element name="StatusMessage" type="string"/> |
| | | <element name="StatusDetail" type="samlp:StatusDetailType"/> |
| | | <complexType name="StatusDetailType"> |
| | | <sequence> |
| | | <any namespace="##any" processContents="lax" minOccurs="0" maxOccurs="unbounded"/> |
| | | </sequence> |
| | | </complexType> |
| | | </schema> |