This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
expression/src/modules/libertyalliance0.py

604 lines
25 KiB
Python

# -*- coding: UTF-8 -*-
# Expression
# By: Frederic Peters <fpeters@entrouvert.com>
# Emmanuel Raviart <eraviart@entrouvert.com>
#
# Copyright (C) 2004 Entr'ouvert, Frederic Peters & Emmanuel Raviart
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Liberty Alliance Module using old Python Lasso library.
This library is a temporary replacement for libertyalliance.py, which will be
removed as soon as the C Lasso library will be ready.
"""
import httplib
import os
import urllib
import urlparse
try:
import lasso0.Protocols.NameRegistration as register
import lasso0.Protocols.SingleLogout as logout
import lasso0.Protocols.SingleSignOnAndFederation as sso
import lasso0.Protocols.FederationTerminationNotification as termination
import lasso0.Schemas.Schema as schema
import lasso0.Schemas.SchemaDom as schemaDom
import lasso0.Soap as soap
import lasso0.Tools as lassoTools
import lasso0.Constants as constants
except ImportError:
sso = None
import libxml2
import libxslt
import expression.core.directories as directories
import expression.core.documents as documents
import expression.core.elements as elements
import expression.core.environs as environs
import expression.core.faults as faults
import expression.core.html as html
import expression.core.http as http
import expression.core.locations as locations
import expression.core.logs as logs
import expression.core.modules as modules
import expression.core.namespaces as namespaces
import expression.core.sessions as sessions
import expression.core.stations as stations
import expression.core.strings as strings
import expression.core.submissions as submissions
import identities
deleteSessionOnLogout = True
#symmetricKey = "Expression rules!" # FIXME
class EntityDescriptor(elements.Element):
def getProviderId(self):
nodes = self.evaluateXpath("@providerID")
if not nodes:
return None
return nodes[0].content
def getSingleSignOnProtocolProfiles(self):
nodes = self.evaluateXpath("md:IDPDescriptor/md:SingleSignOnProtocolProfile")
return [node.content for node in nodes]
def getSingleSignOnServiceUrl(self):
nodes = self.evaluateXpath("md:IDPDescriptor/md:SingleSignOnServiceURL")
if not nodes:
return None
return nodes[0].content
def getSoapEndpoint(self):
nodes = self.evaluateXpath("md:IDPDescriptor/md:SoapEndpoint")
if not nodes:
return None
return nodes[0].content
providerId = property(getProviderId)
singleSignOnProtocolProfiles = property(getSingleSignOnProtocolProfiles)
singleSignOnServiceUrl = property(getSingleSignOnServiceUrl)
soapEndpoint = property(getSoapEndpoint)
class LibertyAlliance(directories.Directory):
def afterAuthentication(self, action = "afterAuthentication", authenticationRequest = None):
"""Liberty Alliance Identity Provider Method which is called once the user authentication
requested by singleSignOn has been attempted.
Handles HTTP GET.
"""
session = environs.getVar("session")
if session is None:
raise "FIXME: Authentication failed. Return the appropriate Liberty Alliance Authentication Response"
if authenticationRequest is None:
# Build authentication request from query stored in session by singleSignOn.
submission = submissions.FakeSubmission(
query = session.libertyAllianceAuthenticationRequestQuery)
keywords = {}
for key in submission.getFieldKeys():
keywords[key] = submission.getField(key)
authenticationRequest = sso.buildAuthenticationRequestFromKeywords(keywords)
user = environs.getVar("user")
if user is None:
raise "FIXME: Authentication failed. Return the appropriate Liberty Alliance Authentication Response"
# Verify the service providerId.
serviceProviderId = authenticationRequest.getProviderID()
serviceProviderMetadata = self.getServiceProviderMetadata(serviceProviderId)
if serviceProviderMetadata is None:
raise "FIXME: Unknown service provider. Return the appropriate Liberty Alliance Authentication Response"
identification = user.getIdentification(serviceProviderId)
# No service identification found, create it.
if identification is None:
if user.identifications is None:
user.node.newTextChild(None, "identifications", None)
identifications = user.identifications
identificationNode = identifications.node.newTextChild(
None, "identification", None)
identification = identifications.newElement(identificationNode)
identification.providerId = serviceProviderId
identification.localNameIdentifier \
= lassoTools.generateNameIdentifier(serviceProviderId)
user.identifications.append(identification)
user.getDocument().save()
# Process the NameIDPolicy.
nameIdentifierPolicy = authenticationRequest.getNameIDPolicy()
nameIdentifier = None
if nameIdentifierPolicy == "none":
# No federation, just return the peer/local name identifier.
nameIdentifier = identification.peerNameIdentifier
if not nameIdentifier:
nameIdentifier = identification.localNameIdentifier
assert nameIdentifier
elif nameIdentifierPolicy == "onetime":
# One time federation.
nameIdentifier = lassoTools.generateNameIdentifier(peerProviderId)
raise "FIXME: Store new nameIdentifier in identification?"
elif nameIdentifierPolicy == "federated":
# Federation. Return peer/local/new name identifier
nameIdentifier = identification.peerNameIdentifier
if not nameIdentifier:
nameIdentifier = identification.localNameIdentifier
assert nameIdentifier
elif nameIdentifierPolicy == "any":
# Federated else none.
consent = authenticationRequest.getConsent()
if consent:
nameIdentifier = identification.peerNameIdentifier
if not nameIdentifier:
nameIdentifier = identification.localNameIdentifier
assert nameIdentifier
else:
raise "FIXME: No consent given by the principal"
# Get nameIdentifiers formats.
nameIdentifierFormat = "federated"
idpNameIdentifierFormat = "federated"
if nameIdentifierPolicy == "onetime":
nameIdentifierFormat = "onetime"
idpNameIdentifierFormat = "onetime"
providerId = self.metadata.providerId
# Build assertion.
assertion = sso.buildAuthenticationAssertion(
authnRequest = authenticationRequest,
issuer = providerId,
authenticationMethod = session.authenticationMethod,
nameIdentifier = nameIdentifier,
nameQualifier = serviceProviderId,
nameIdentifierFormat = nameIdentifierFormat,
idpNameIdentifier = identification.localNameIdentifier,
idpNameQualifier = providerId,
idpNameIdentifierFormat = idpNameIdentifierFormat)
# Save the peerProviderId.
session.peerProviderId = serviceProviderId
# Answer to the service provider.
profile = authenticationRequest.getProtocolProfile()
if profile == "artifact":
# Send an artifact, keep the assertion for later retrieval.
raise "BOUM"
assertionsProxy = getProxyForServerRole("assertions")
artifact = assertionsProxy.addAssertion(assertion.exportToString())
url = sso.getServiceProviderAssertionArtifactHandlerUrl(
serviceProviderMetadata.assertionConsumerServiceUrl, # FIXME
artifact,
authenticationRequest.getRelayState())
return redirect(url)
elif profile == "post":
# Send an authentication response using a HTML form.
authenticationResponse \
= sso.buildAuthenticationResponseFromAuthnRequest(
providerId = providerId,
statusCode = "Success",
assertion = assertion,
authnRequest = authenticationRequest)
authenticationResponseEmbedded \
= authenticationResponse.exportToEncodedUrlEmbeddedMessage()
layout = html.html(
html.head(html.title(_("Authentication Succeeded"))),
html.body(
html.form(
html.input(
name = "LARES", type = "hidden",
value = authenticationResponseEmbedded),
html.p(_("""\
You have been succesfully authenticated; click ok to go back to the service
provider.\
""")),
html.div(
html.span(
html.input(
class_ = "button", name = "submit",
type = "submit", value = _("OK")),
class_ = "action-buttons-bar"),
class_ = "buttons-bar"),
action = "https://service-provider:2003/liberty-alliance/processAuthenticationResponse",
# FIXME
enctype = "multipart/form-data", method = "post")))
htmlDocument = documents.newTemporaryHtmlDocument()
htmlDocument.append(layout)
stylesheet = self.getDataHolder().getSiteXslt()
self.outputHttpHtmlDocument(htmlDocument, stylesheet)
else:
# Unknown profile. We dont know what to do :
raise "FIXME: Unknow protocol profile"
def getCertificateFilePath(self):
nodes = self.evaluateXpath("yep:certificateFile")
if nodes:
return os.path.normpath(os.path.join(self.getAbsolutePath(), nodes[0].content.strip()))
else:
return None
def getCertificationAuthorityCertificateFilePath(self):
nodes = self.evaluateXpath("yep:caCertificateFile")
if nodes:
return os.path.normpath(os.path.join(self.getAbsolutePath(), nodes[0].content.strip()))
else:
return None
def getIdentityProviderMetadata(self):
# FIXME: Temporary element name: yep:identityProviderMetadata
nodes = self.evaluateXpath("yep:identityProvider/@src")
if not nodes:
return None
location = nodes[0].content.strip()
try:
metadataHolder = self.walkToLocation(self.getSubPathInternUri(
location))
except faults.PathNotFound:
return None
return metadataHolder.getRootElement()
def getMetadata(self):
try:
metadataHolder = self.walkToLocation(self.getSubPathInternUri("metadata.xml"))
except faults.PathNotFound:
return None
return metadataHolder.getRootElement()
def getPeerPublicKeyFilePath(self):
nodes = self.evaluateXpath("yep:peerPublicKeyFile")
if nodes:
return os.path.normpath(os.path.join(self.getAbsolutePath(), nodes[0].content.strip()))
else:
return None
def getPrivateKeyFilePath(self):
nodes = self.evaluateXpath("yep:privateKeyFile")
if nodes:
return os.path.normpath(os.path.join(self.getAbsolutePath(), nodes[0].content.strip()))
else:
return None
def getServiceProviderMetadata(self, providerId):
try:
metadataHolder = self.walkToLocation(self.getSubPathInternUri(
"service-providers/%s" % strings.simplify(providerId)))
except faults.PathNotFound:
logs.debug("Service provider not found: %s" % providerId)
return None
return metadataHolder.getRootElement()
def login(self):
"""Liberty Alliance service provider method that builds an
authentication requests and then redirects to the identity provider
login page.
Handles HTTP GET.
"""
command = environs.getVar("httpCommand")
if command != "GET":
raise faults.PathNotFound(environs.getVar("currentStation"), "")
identityProviderMetadata = self.identityProviderMetadata
assert identityProviderMetadata is not None
submission = environs.getVar("submission")
nextUrl = submission.getField("nextUrl")
if not nextUrl:
nextUrl = None
# Compute the crypted relayState from nextUrl.
if nextUrl:
# FIXME: Encrypt relayState.
# relayState = lassoTools.encryptRelayState(nextUrl, symmetricKey)
relayState = nextUrl
else:
relayState = None
cryptedRelayState = relayState # FIXME: To do.
providerId = self.metadata.providerId
ssoUrl = sso.getIdentityProviderAuthenticationUrl(
authenticationMethods = (
"smartcardPki",
"softwarePki",
"password"),
profile = "post",
nameIdPolicy = "federated",
relayState = cryptedRelayState,
serviceProviderId = providerId,
singleSignOnServiceUrl = identityProviderMetadata.singleSignOnServiceUrl)
return environs.getVar("httpRequestHandler").outputRedirect(
self, ssoUrl)
def processAuthenticationResponse(self):
"""Liberty Alliance Service Provider Method which processes an
authentication response coming (indirectly) from identity provider.
Handles HTTP GET & POST
"""
submission = environs.getVar("submission")
command = environs.getVar("httpCommand")
if command == "GET":
if submission.hasField("SAMLArt"):
# Artifact.
if keywords.has_key("RelayState"):
relayState = keywords["RelayState"]
del keywords["RelayState"]
else:
relayState = None
assertionArtifact = sso.buildAssertionArtifactFromKeywords(
keywords)
request = sso.buildRequest(assertionArtifact)
# FIXME: Don't use identityProviderLocation. Use instead
# IdentityProviderSuccintID, which is encoded into the artifact.
response = self.sendSoapRequest(request)
else:
# Error, unknown profile.
response = sso.buildAuthenticationResponseFromKeywords(keywords)
elif command == "POST":
if submission.hasField("LARES"):
authenticationResponseEmbedded = submission.getField("LARES")
# Remove base64 url encoding.
authenticationResponseEmbeddedDecoded \
= schema.decodeEncodedUrlEmbeddedMessage(
authenticationResponseEmbedded)
authenticationResponseKeywords = {}
for segment in authenticationResponseEmbeddedDecoded.split("&"):
keyAndValue = segment.split("=", 1)
if len(keyAndValue) != 2:
logs.info(
"""Wrong segment "segment" in response.""" % segment)
continue
key, value = keyAndValue
authenticationResponseKeywords[key] = value
response = sso.buildAuthenticationResponseFromKeywords(
authenticationResponseKeywords)
else:
# Error, unknown profile.
response = sso.buildAuthenticationResponseFromKeywords(keywords)
else:
raise faults.PathNotFound(environs.getVar("currentStation"), "")
relayState = response.getRelayState()
statusCode = response.getStatusCode()
if statusCode != "Success":
message = _("Liberty Alliance authentication response"
" failed (reason = %s).") % statusCode
return environs.getVar("httpRequestHandler").outputAlert(
self, message, title = N_("Failure"), url = self.constructUri("/"))
assertion = response.getAssertion()
idpNameIdentifier = assertion.getIDPProvidedNameIdentifier()
nameIdentifier = assertion.getNameIdentifier()
identityProviderMetadata = self.identityProviderMetadata
identityProviderId = identityProviderMetadata.providerId
# FIXME? Use providerId stored in response.
identitiesDirectoryHolder = self.walkToLocation("/identities")
identitiesDirectory = identitiesDirectoryHolder.getRootElement()
if nameIdentifier and nameIdentifier != idpNameIdentifier:
user = identitiesDirectory.checkLocalNameIdentifier(
identityProviderId, nameIdentifier)
else:
user = identitiesDirectory.checkPeerNameIdentifier(
identityProviderId, idpNameIdentifier)
if user is None:
identityFeature = modules.getElementFeature(
namespaces.yep.uri, "identity")
userHolder = identityFeature.newXmlHolder(identitiesDirectoryHolder)
user = userHolder.getRootElement()
## user.language = environs.getVar("readLanguages")[0]
user.node.newTextChild(None, "identifications", None)
identifications = user.identifications
identificationNode = identifications.node.newTextChild(
None, "identification", None)
identification = identifications.newElement(identificationNode)
user.identifications.append(identification)
identification.providerId = identityProviderId
if nameIdentifier and nameIdentifier != idpNameIdentifier:
identification.localNameIdentifier = nameIdentifier
identification.peerNameIdentifier = idpNameIdentifier
# The following line is done below.
# user.getDocument().save()
session = sessions.getOrCreateSession()
session.publishToken = True
session.peerProviderId = identityProviderId
session.authenticationMethod = assertion.getAuthenticationMethod()
session.userLocation = user.getUriInternPath()
user.sessionLocation = session.getUriInternPath()
user.getDocument().save()
baseEnviron = environs.get(_level = "handleHttpCommand")
baseEnviron.setVar("user", user)
logs.debug(
"""Setting session user = "%s" at location "%s".""" % (
user.simpleLabel, session.userLocation))
## # Change the Name Identifier :
## # change in local data :
## peerProviderId = session["peerProviderId"]
## localNameIdentifier = self.getLocalNameIdentifier(peerProviderId)
## peerNameIdentifier = self.getPeerNameIdentifier(peerProviderId)
## newLocalNameIdentifier = lassoTools.generateNameIdentifier(
## peerProviderId)
## self.setLocalNameIdentifier(peerProviderId, newLocalNameIdentifier)
## # change to the remote IDP :
## registerRequest = register.buildRegisterNameIdentifierRequest(
## peerProviderId,
## peerNameIdentifier, newLocalNameIdentifier, localNameIdentifier)
## response = self.sendSoapRequestMessage(registerRequest)
## # FIXME : process the response
if relayState:
# FIXME: Uncrypt relayState.
# nextUrl = lassoTools.decryptRelayState(relayState, symmetricKey)
nextUrl = relayState
else:
nextUrl = None
if not nextUrl:
nextUrl = "/"
nextUrl = self.constructUri(nextUrl)
return environs.getVar("httpRequestHandler").outputRedirect(
self, nextUrl)
def singleSignOn(self):
"""Liberty Alliance Identity Provider Method which processes an
authentication request coming (indirectly) from a service provider.
"""
command = environs.getVar("httpCommand")
if command != "GET":
raise faults.PathNotFound(environs.getVar("currentStation"), "")
# Build the Authentication Request.
submission = environs.getVar("submission")
keywords = {}
for key in submission.getFieldKeys():
keywords[key] = submission.getField(key)
authenticationRequest = sso.buildAuthenticationRequestFromKeywords(keywords)
# Verify the providerId.
serviceProviderId = authenticationRequest.getProviderID()
serviceProviderMetadata = self.getServiceProviderMetadata(serviceProviderId)
if serviceProviderMetadata is None:
raise "FIXME: Unknown service provider. Return the appropriate Liberty Alliance Authentication Response"
# Verify the signature of the AuthnRequest if needed.
# authnRequestSigned = serviceProviderMetadata.authnRequestSigned
# if authnRequestSigned:
# # FIXME: To do.
# pass
# Process the authentication method.
authenticationMethods = authenticationRequest.getAuthenticationMethods(
)
strongerMethods = ["smartcardPki", "softwarePki", "password"]
chosenAuthenticationMethod = "smartcardPki"
if len(authenticationMethods) > 0:
# choose the default authentication of the IDP :
authenticationComparison = \
authenticationRequest.getAuthnContextComparison()
if authenticationComparison == "exact":
# choose the exactly requested method :
for method in authenticationMethods:
if method in strongerMethods:
chosenAuthenticationMethod = method
break
elif authenticationComparison == "minimum":
# choose a method as strong as the one requested:
for method in strongerMethods:
if method in authenticationMethods:
chosenAuthenticationMethod = method
break
elif authenticationComparison == "better":
# FIXME : ...
pass
logs.debug("Authentication method: %s" % chosenAuthenticationMethod)
# FIXME : define a way to know if the authentication process
# can be passive or not.
# process isPassive and forceAuthn :
isPassive, forceAuthentication = sso.processAuthenticationRequest(
authenticationRequest)
user = environs.getVar("user")
if isPassive:
# do not interact with the principal :
if user is None:
url = sso.getServiceProviderAssertionArtifactHandlerUrl(
serviceProviderMetadata.assertionConsumerServiceUrl, # FIXME
sso.buildStatus("noPassive"))
return environs.getVar("httpRequestHandler").outputRedirect(
self, url)
return self.afterAuthentication(
"singleSignOn", authenticationRequest)
elif forceAuthentication or user is None:
# Force user authentication even if he was already authenticated.
# FIXME: Use chosenAuthenticationMethod to select the
# authentication method to use.
# Save the query. It will be used after authentication.
session = sessions.getOrCreateSession()
session.publishToken = True
query = environs.getVar("httpQuery")
session.libertyAllianceAuthenticationRequestQuery = query
return self.walkToLocation(
"/accounts/passwords/login", "GET",
nextUrl = self.getActionUri("afterAuthentication"))
else:
# User is already authenticated and there is no need to
# reauthenticate him.
return self.afterAuthentication(
"singleSignOn", authenticationRequest)
certificateFilePath = property(getCertificateFilePath)
certificationAuthorityCertificateFilePath = property(
getCertificationAuthorityCertificateFilePath)
identityProviderMetadata = property(getIdentityProviderMetadata)
metadata = property(getMetadata)
peerPublicKeyFilePath = property(getPeerPublicKeyFilePath)
privateKeyFilePath = property(getPrivateKeyFilePath)
elements.registerElement(
namespaces.md.uri, "EntityDescriptor", EntityDescriptor,
"http://www.entrouvert.org/expression/schemas/liberty-metadata-v1.0.xsd")
elements.registerElement(
namespaces.yep.uri, "libertyAlliance", LibertyAlliance,
"http://www.entrouvert.org/expression/schemas/Directory.xsd",
"http://www.entrouvert.org/expression/descriptions/Directory.xml")