From 25ea887500da7046740540f0622635940eb03257 Mon Sep 17 00:00:00 2001 From: Scott Cantor Date: Tue, 18 Oct 2005 21:27:51 +0000 Subject: [PATCH] Initial ADFS check in, compiles, but not tested. --- adfs/.gitignore | 12 ++ adfs/Makefile.am | 26 +++ adfs/XML.cpp | 62 +++++++ adfs/adfs.cpp | 329 +++++++++++++++++++++++++++++++++ adfs/adfs.dsp | 119 ++++++++++++ adfs/adfs.rc | 109 +++++++++++ adfs/handlers.cpp | 479 ++++++++++++++++++++++++++++++++++++++++++++++++ adfs/internal.h | 92 ++++++++++ adfs/listener.cpp | 539 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ adfs/resource.h | 15 ++ 10 files changed, 1782 insertions(+) create mode 100644 adfs/.gitignore create mode 100644 adfs/Makefile.am create mode 100644 adfs/XML.cpp create mode 100644 adfs/adfs.cpp create mode 100644 adfs/adfs.dsp create mode 100644 adfs/adfs.rc create mode 100644 adfs/handlers.cpp create mode 100644 adfs/internal.h create mode 100644 adfs/listener.cpp create mode 100644 adfs/resource.h diff --git a/adfs/.gitignore b/adfs/.gitignore new file mode 100644 index 0000000..db379c7 --- /dev/null +++ b/adfs/.gitignore @@ -0,0 +1,12 @@ +/Makefile.in +/Makefile +/.libs +/.deps +/*.lo +/*.la +/Debug +/*.plg +/Release +/*.dep +/*.mak +/*.aps \ No newline at end of file diff --git a/adfs/Makefile.am b/adfs/Makefile.am new file mode 100644 index 0000000..a1fda6a --- /dev/null +++ b/adfs/Makefile.am @@ -0,0 +1,26 @@ +## $Id$ + +AUTOMAKE_OPTIONS = foreign + +plugindir = $(libexecdir) +plugin_LTLIBRARIES = adfs.la + +noinst_HEADERS = internal.h + +adfs_la_LIBADD = \ + ../shib/libshib.la \ + ../shib-target/libshib-target.la + +adfs_la_SOURCES = \ + adfs.cpp \ + listener.cpp \ + handlers.cpp \ + XML.cpp + + +adfs_la_LDFLAGS = -module -avoid-version + +install-exec-hook: + for la in $(plugin_LTLIBRARIES) ; do rm -f $(DESTDIR)$(plugindir)/$$la ; done + +EXTRA_DIST = adfs.dsp resource.h adfs.rc diff --git a/adfs/XML.cpp b/adfs/XML.cpp new file mode 100644 index 0000000..ea8653a --- /dev/null +++ b/adfs/XML.cpp @@ -0,0 +1,62 @@ +/* + * Copyright 2001-2005 Internet2 + * + * 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 + * + * http://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. + */ + +/* XML.cpp - XML constants + + Scott Cantor + 6/4/02 + + $History:$ +*/ + +#include "internal.h" + +// Namespace and schema string literals + +const XMLCh adfs::XML::WSFED_NS[] = // http://schemas.xmlsoap.org/ws/2003/07/secext +{ chLatin_h, chLatin_t, chLatin_t, chLatin_p, chColon, chForwardSlash, chForwardSlash, + chLatin_s, chLatin_c, chLatin_h, chLatin_e, chLatin_m, chLatin_a, chLatin_s, chPeriod, + chLatin_x, chLatin_m, chLatin_l, chLatin_s, chLatin_o, chLatin_a, chLatin_p, chPeriod, + chLatin_o, chLatin_r, chLatin_g, chForwardSlash, chLatin_w, chLatin_s, chForwardSlash, + chDigit_2, chDigit_0, chDigit_0, chDigit_3, chForwardSlash, chDigit_0, chDigit_7, chForwardSlash, + chLatin_s, chLatin_e, chLatin_c, chLatin_e, chLatin_x, chLatin_t, chNull +}; + +const XMLCh adfs::XML::WSTRUST_NS[] = // http://schemas.xmlsoap.org/ws/2005/02/trust +{ chLatin_h, chLatin_t, chLatin_t, chLatin_p, chColon, chForwardSlash, chForwardSlash, + chLatin_s, chLatin_c, chLatin_h, chLatin_e, chLatin_m, chLatin_a, chLatin_s, chPeriod, + chLatin_x, chLatin_m, chLatin_l, chLatin_s, chLatin_o, chLatin_a, chLatin_p, chPeriod, + chLatin_o, chLatin_r, chLatin_g, chForwardSlash, chLatin_w, chLatin_s, chForwardSlash, + chDigit_2, chDigit_0, chDigit_0, chDigit_5, chForwardSlash, chDigit_0, chDigit_2, chForwardSlash, + chLatin_t, chLatin_r, chLatin_u, chLatin_s, chLatin_t, chNull +}; + +const XMLCh adfs::XML::WSTRUST_SCHEMA_ID[] = +{ chLatin_W, chLatin_S, chDash, chLatin_T, chLatin_r, chLatin_u, chLatin_s, chLatin_t, chPeriod, chLatin_x, chLatin_s, chLatin_d, chNull +}; + +const XMLCh adfs::XML::Literals::RequestedSecurityToken[] = +{ chLatin_R, chLatin_e, chLatin_q, chLatin_u, chLatin_e, chLatin_s, chLatin_t, chLatin_e, chLatin_d, + chLatin_S, chLatin_e, chLatin_c, chLatin_u, chLatin_r, chLatin_i, chLatin_t, chLatin_y, + chLatin_T, chLatin_o, chLatin_k, chLatin_e, chLatin_n, chNull +}; + +const XMLCh adfs::XML::Literals::RequestSecurityTokenResponse[] = +{ chLatin_R, chLatin_e, chLatin_q, chLatin_u, chLatin_e, chLatin_s, chLatin_t, + chLatin_S, chLatin_e, chLatin_c, chLatin_u, chLatin_r, chLatin_i, chLatin_t, chLatin_y, + chLatin_T, chLatin_o, chLatin_k, chLatin_e, chLatin_n, + chLatin_R, chLatin_e, chLatin_s, chLatin_p, chLatin_o, chLatin_n, chLatin_s, chLatin_e, chNull +}; diff --git a/adfs/adfs.cpp b/adfs/adfs.cpp new file mode 100644 index 0000000..2f35433 --- /dev/null +++ b/adfs/adfs.cpp @@ -0,0 +1,329 @@ +/* + * Copyright 2001-2005 Internet2 + * + * 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 + * + * http://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. + */ + +/* adfs.cpp - bootstraps the ADFS extension library + + Scott Cantor + 10/10/05 +*/ + +#ifdef WIN32 +# define ADFS_EXPORTS __declspec(dllexport) +#else +# define ADFS_EXPORTS +#endif + +#include "internal.h" + +#include + + +using namespace std; +using namespace saml; +using namespace shibboleth; +using namespace adfs; +using namespace log4cpp; + +// Plugin Factories +PlugManager::Factory ADFSListenerFactory; +PlugManager::Factory ADFSSessionInitiatorFactory; +PlugManager::Factory ADFSHandlerFactory; + + +extern "C" int ADFS_EXPORTS saml_extension_init(void*) +{ + // Register extension schema. + saml::XML::registerSchema(adfs::XML::WSTRUST_NS,adfs::XML::WSTRUST_SCHEMA_ID); + + // Register plugin factories (some override existing Shib functionality). + SAMLConfig& conf=SAMLConfig::getConfig(); + conf.getPlugMgr().regFactory(shibtarget::XML::MemoryListenerType,&ADFSListenerFactory); + + auto_ptr_char temp1(Constants::SHIB_SESSIONINIT_PROFILE_URI); + conf.getPlugMgr().regFactory(temp1.get(),&ADFSSessionInitiatorFactory); + + auto_ptr_char temp2(adfs::XML::WSFED_NS); + conf.getPlugMgr().regFactory(temp2.get(),&ADFSHandlerFactory); + + return 0; +} + +extern "C" void ADFS_EXPORTS saml_extension_term() +{ + // Unregister metadata factories + SAMLConfig& conf=SAMLConfig::getConfig(); + conf.getPlugMgr().unregFactory(shibtarget::XML::MemoryListenerType); + + auto_ptr_char temp1(Constants::SHIB_SESSIONINIT_PROFILE_URI); + conf.getPlugMgr().unregFactory(temp1.get()); + + auto_ptr_char temp2(adfs::XML::WSFED_NS); + conf.getPlugMgr().unregFactory(temp2.get()); +} + +// For now, we'll just put the meat of the profile here. + +SAMLAuthenticationStatement* adfs::checkAssertionProfile(const SAMLAssertion* a) +{ + // Is it signed? + if (!a->isSigned()) + throw FatalProfileException("rejected unsigned ADFS assertion"); + + // Is it valid? + time_t now=time(NULL); + SAMLConfig& config=SAMLConfig::getConfig(); + if (a->getIssueInstant()->getEpoch() < now-(2*config.clock_skew_secs)) + throw ExpiredAssertionException("rejected expired ADFS assertion"); + + const SAMLDateTime* notBefore=a->getNotBefore(); + const SAMLDateTime* notOnOrAfter=a->getNotOnOrAfter(); + if (!notBefore || !notOnOrAfter) + throw ExpiredAssertionException("rejected ADFS assertion without time conditions"); + if (now+config.clock_skew_secs < notBefore->getEpoch()) + throw ExpiredAssertionException("rejected ADFS assertion that is not yet valid"); + if (notOnOrAfter->getEpoch() <= now-config.clock_skew_secs) + throw ExpiredAssertionException("rejected expired ADFS assertion"); + + // Look for an authentication statement. + SAMLAuthenticationStatement* as=NULL; + for (Iterator statements=a->getStatements(); !as && statements.hasNext();) + as=dynamic_cast(statements.next()); + if (!as) + throw FatalProfileException("rejecting ADFS assertion without authentication statement"); + + return as; +} + +/************************************************************************* + * CGI Parser implementation + */ + +CgiParse::CgiParse(const char* data, unsigned int len) +{ + const char* pch = data; + unsigned int cl = len; + + while (cl && pch) { + char *name; + char *value; + value=fmakeword('&',&cl,&pch); + plustospace(value); + url_decode(value); + name=makeword(value,'='); + kvp_map[name]=value; + free(name); + } +} + +CgiParse::~CgiParse() +{ + for (map::iterator i=kvp_map.begin(); i!=kvp_map.end(); i++) + free(i->second); +} + +const char* +CgiParse::get_value(const char* name) const +{ + map::const_iterator i=kvp_map.find(name); + if (i==kvp_map.end()) + return NULL; + return i->second; +} + +/* Parsing routines modified from NCSA source. */ +char * +CgiParse::makeword(char *line, char stop) +{ + int x = 0,y; + char *word = (char *) malloc(sizeof(char) * (strlen(line) + 1)); + + for(x=0;((line[x]) && (line[x] != stop));x++) + word[x] = line[x]; + + word[x] = '\0'; + if(line[x]) + ++x; + y=0; + + while(line[x]) + line[y++] = line[x++]; + line[y] = '\0'; + return word; +} + +char * +CgiParse::fmakeword(char stop, unsigned int *cl, const char** ppch) +{ + int wsize; + char *word; + int ll; + + wsize = 1024; + ll=0; + word = (char *) malloc(sizeof(char) * (wsize + 1)); + + while(1) + { + word[ll] = *((*ppch)++); + if(ll==wsize-1) + { + word[ll+1] = '\0'; + wsize+=1024; + word = (char *)realloc(word,sizeof(char)*(wsize+1)); + } + --(*cl); + if((word[ll] == stop) || word[ll] == EOF || (!(*cl))) + { + if(word[ll] != stop) + ll++; + word[ll] = '\0'; + return word; + } + ++ll; + } +} + +void +CgiParse::plustospace(char *str) +{ + register int x; + + for(x=0;str[x];x++) + if(str[x] == '+') str[x] = ' '; +} + +char +CgiParse::x2c(char *what) +{ + register char digit; + + digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A')+10 : (what[0] - '0')); + digit *= 16; + digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A')+10 : (what[1] - '0')); + return(digit); +} + +void +CgiParse::url_decode(char *url) +{ + register int x,y; + + for(x=0,y=0;url[y];++x,++y) + { + if((url[x] = url[y]) == '%') + { + url[x] = x2c(&url[y+1]); + y+=2; + } + } + url[x] = '\0'; +} + +static inline char hexchar(unsigned short s) +{ + return (s<=9) ? ('0' + s) : ('A' + s - 10); +} + +string CgiParse::url_encode(const char* s) +{ + static char badchars[]="\"\\+<>#%{}|^~[]`;/?:@=&"; + + string ret; + for (; *s; s++) { + if (strchr(badchars,*s) || *s<=0x20 || *s>=0x7F) { + ret+='%'; + ret+=hexchar(*s >> 4); + ret+=hexchar(*s & 0x0F); + } + else + ret+=*s; + } + return ret; +} + +// CDC implementation + +const char CommonDomainCookie::CDCName[] = "_saml_idp"; + +CommonDomainCookie::CommonDomainCookie(const char* cookie) +{ + if (!cookie) + return; + + Category& log=Category::getInstance(ADFS_LOGCAT".CommonDomainCookie"); + + // Copy it so we can URL-decode it. + char* b64=strdup(cookie); + CgiParse::url_decode(b64); + + // Chop it up and save off elements. + vector templist; + char* ptr=b64; + while (*ptr) { + while (*ptr && isspace(*ptr)) ptr++; + char* end=ptr; + while (*end && !isspace(*end)) end++; + templist.push_back(string(ptr,end-ptr)); + ptr=end; + } + free(b64); + + // Now Base64 decode the list. + for (vector::iterator i=templist.begin(); i!=templist.end(); i++) { + unsigned int len; + XMLByte* decoded=Base64::decode(reinterpret_cast(i->c_str()),&len); + if (decoded && *decoded) { + m_list.push_back(reinterpret_cast(decoded)); + XMLString::release(&decoded); + } + else + log.warn("cookie element does not appear to be base64-encoded"); + } +} + +const char* CommonDomainCookie::set(const char* providerId) +{ + // First scan the list for this IdP. + for (vector::iterator i=m_list.begin(); i!=m_list.end(); i++) { + if (*i == providerId) { + m_list.erase(i); + break; + } + } + + // Append it to the end. + m_list.push_back(providerId); + + // Now rebuild the delimited list. + string delimited; + for (vector::const_iterator j=m_list.begin(); j!=m_list.end(); j++) { + if (!delimited.empty()) delimited += ' '; + + unsigned int len; + XMLByte* b64=Base64::encode(reinterpret_cast(j->c_str()),j->length(),&len); + XMLByte *pos, *pos2; + for (pos=b64, pos2=b64; *pos2; pos2++) + if (isgraph(*pos2)) + *pos++=*pos2; + *pos=0; + + delimited += reinterpret_cast(b64); + XMLString::release(&b64); + } + + m_encoded=CgiParse::url_encode(delimited.c_str()); + return m_encoded.c_str(); +} diff --git a/adfs/adfs.dsp b/adfs/adfs.dsp new file mode 100644 index 0000000..78f7d36 --- /dev/null +++ b/adfs/adfs.dsp @@ -0,0 +1,119 @@ +# Microsoft Developer Studio Project File - Name="adfs" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102 + +CFG=adfs - Win32 Debug +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "adfs.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "adfs.mak" CFG="adfs - Win32 Debug" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "adfs - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library") +!MESSAGE "adfs - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +MTL=midl.exe +RSC=rc.exe + +!IF "$(CFG)" == "adfs - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "Release" +# PROP BASE Intermediate_Dir "Release" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "Release" +# PROP Intermediate_Dir "Release" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "ADFS_EXPORTS" /YX /FD /c +# ADD CPP /nologo /MD /W3 /GR /GX /O2 /I ".." /I "..\..\..\opensaml\c" /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /YX /FD /c +# ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /d "NDEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 +# ADD LINK32 log4cpp.lib xerces-c_2.lib saml_5.lib /nologo /dll /machine:I386 /out:"Release/adfs.so" /libpath:"..\..\..\opensaml\c\saml\Release" + +!ELSEIF "$(CFG)" == "adfs - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "Debug" +# PROP BASE Intermediate_Dir "Debug" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "Debug" +# PROP Intermediate_Dir "Debug" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "ADFS_EXPORTS" /YX /FD /GZ /c +# ADD CPP /nologo /MDd /W3 /Gm /GR /GX /ZI /Od /I ".." /I "..\..\..\opensaml\c" /D "_WINDOWS" /D "_WINDLL" /D "WIN32" /D "_DEBUG" /D "_MBCS" /FR /YX /FD /GZ /c +# ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /d "_DEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /pdbtype:sept +# ADD LINK32 log4cppD.lib xerces-c_2D.lib saml_5D.lib /nologo /dll /debug /machine:I386 /out:"Debug/adfs.so" /pdbtype:sept /libpath:"..\..\..\opensaml\c\saml\Debug" + +!ENDIF + +# Begin Target + +# Name "adfs - Win32 Release" +# Name "adfs - Win32 Debug" +# Begin Source File + +SOURCE=.\adfs.cpp +# End Source File +# Begin Source File + +SOURCE=.\adfs.rc +# End Source File +# Begin Source File + +SOURCE=.\handlers.cpp +# End Source File +# Begin Source File + +SOURCE=.\internal.h +# End Source File +# Begin Source File + +SOURCE=.\listener.cpp +# End Source File +# Begin Source File + +SOURCE=.\resource.h +# End Source File +# Begin Source File + +SOURCE=.\XML.cpp +# End Source File +# End Target +# End Project diff --git a/adfs/adfs.rc b/adfs/adfs.rc new file mode 100644 index 0000000..d1b2bca --- /dev/null +++ b/adfs/adfs.rc @@ -0,0 +1,109 @@ +//Microsoft Developer Studio generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "afxres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE DISCARDABLE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE DISCARDABLE +BEGIN + "#include ""afxres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE DISCARDABLE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +#ifndef _MAC +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 1,3,0,0 + PRODUCTVERSION 1,3,0,0 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "Comments", "\0" + VALUE "CompanyName", "Internet2\0" + VALUE "FileDescription", "Shibboleth ADFS Plugins\0" + VALUE "FileVersion", "1, 3, 0, 0\0" + VALUE "InternalName", "adfs\0" + VALUE "LegalCopyright", "Copyright © 2005 Internet2\0" + VALUE "LegalTrademarks", "\0" + VALUE "OriginalFilename", "adfs.so\0" + VALUE "PrivateBuild", "\0" + VALUE "ProductName", "Shibboleth 1.3\0" + VALUE "ProductVersion", "1, 3, 0, 0\0" + VALUE "SpecialBuild", "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + +#endif // !_MAC + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/adfs/handlers.cpp b/adfs/handlers.cpp new file mode 100644 index 0000000..d23b044 --- /dev/null +++ b/adfs/handlers.cpp @@ -0,0 +1,479 @@ +/* + * Copyright 2001-2005 Internet2 + * + * 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 + * + * http://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. + */ + +/* + * handlers.cpp -- ADFS-aware profile handlers that plug into SP + * + * Scott Cantor + * 10/10/2005 + */ + +#include "internal.h" + +#ifndef HAVE_STRCASECMP +# define strcasecmp stricmp +#endif + +using namespace std; +using namespace saml; +using namespace shibboleth; +using namespace shibtarget; +using namespace adfs; +using namespace log4cpp; + +namespace { + + // TODO: Refactor/extend API so I don't have to cut/paste this code out of libshib-target + class SessionInitiator : virtual public IHandler + { + public: + SessionInitiator(const DOMElement* e) {} + ~SessionInitiator() {} + pair run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true); + + private: + const IPropertySet* getCompatibleACS(const IApplication* app, const vector& profiles); + pair ShibAuthnRequest( + ShibTarget* st, + const IPropertySet* shire, + const char* dest, + const char* target, + const char* providerId + ); + pair ADFSAuthnRequest( + ShibTarget* st, + const IPropertySet* shire, + const char* dest, + const char* target, + const char* providerId + ); + }; + + class ADFSHandler : virtual public IHandler + { + public: + ADFSHandler(const DOMElement* e) {} + ~ADFSHandler() {} + pair run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true); + }; +} + + +IPlugIn* ADFSSessionInitiatorFactory(const DOMElement* e) +{ + return new SessionInitiator(e); +} + +IPlugIn* ADFSHandlerFactory(const DOMElement* e) +{ + return new ADFSHandler(e); +} + +pair SessionInitiator::run(ShibTarget* st, const IPropertySet* handler, bool isHandler) +{ + string dupresource; + const char* resource=NULL; + const IPropertySet* ACS=NULL; + const IApplication* app=st->getApplication(); + + if (isHandler) { + /* + * Binding is CGI query string with: + * target the resource to direct back to later + * acsIndex optional index of an ACS to use on the way back in + * providerId optional direct invocation of a specific IdP + */ + string query=st->getArgs(); + CgiParse parser(query.c_str(),query.length()); + + const char* option=parser.get_value("acsIndex"); + if (option) + ACS=app->getAssertionConsumerServiceByIndex(atoi(option)); + option=parser.get_value("providerId"); + + resource=parser.get_value("target"); + if (!resource || !*resource) { + pair home=app->getString("homeURL"); + if (home.first) + resource=home.second; + else + throw FatalProfileException("Session initiator requires a target parameter or a homeURL application property."); + } + else if (!option) { + dupresource=resource; + resource=dupresource.c_str(); + } + + if (option) { + // Here we actually use metadata to invoke the SSO service directly. + Metadata m(app->getMetadataProviders()); + const IEntityDescriptor* entity=m.lookup(option); + if (!entity) + throw MetadataException("Session initiator unable to locate metadata for provider ($1).", params(1,option)); + + // Look for an IdP role with Shib support. + const IIDPSSODescriptor* role=entity->getIDPSSODescriptor(Constants::SHIB_NS); + if (role) { + // Look for a SSO endpoint with Shib support. + const IEndpointManager* SSO=role->getSingleSignOnServiceManager(); + const IEndpoint* ep=SSO->getEndpointByBinding(Constants::SHIB_AUTHNREQUEST_PROFILE_URI); + if (ep) { + if (!ACS) { + // Look for an ACS with SAML support. + vector v; + v.push_back(SAML11_POST); + v.push_back(SAML11_ARTIFACT); + v.push_back(SAML10_ARTIFACT); + v.push_back(SAML10_POST); + ACS=getCompatibleACS(app,v); + } + auto_ptr_char dest(ep->getLocation()); + return ShibAuthnRequest( + st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second + ); + } + } + // Look for an IdP role with ADFS support. + role=entity->getIDPSSODescriptor(adfs::XML::WSFED_NS); + if (role) { + // Finally, look for a SSO endpoint with ADFS support. + const IEndpointManager* SSO=role->getSingleSignOnServiceManager(); + const IEndpoint* ep=SSO->getEndpointByBinding(adfs::XML::WSFED_NS); + if (ep) { + if (!ACS) { + // Look for an ACS with ADFS support. + vector v; + v.push_back(ADFS_SSO); + ACS=getCompatibleACS(app,v); + } + auto_ptr_char dest(ep->getLocation()); + return ADFSAuthnRequest( + st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second + ); + } + } + + throw MetadataException( + "Session initiator unable to locate a compatible identity provider SSO endpoint for provider ($1).", + params(1,option) + ); + } + } + else { + // We're running as a "virtual handler" from within the filter. + // The target resource is the current one and everything else is defaulted. + resource=st->getRequestURL(); + } + + // For now, we only support external session initiation via a wayfURL + pair wayfURL=handler->getString("wayfURL"); + if (!wayfURL.first) + throw ConfigurationException("Session initiator is missing wayfURL property."); + + pair wayfBinding=handler->getXMLString("wayfBinding"); + if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,Constants::SHIB_AUTHNREQUEST_PROFILE_URI)) + // Standard Shib 1.x + return ShibAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second); + else if (!XMLString::compareString(wayfBinding.second,Constants::SHIB_LEGACY_AUTHNREQUEST_PROFILE_URI)) + // Shib pre-1.2 + return ShibAuthnRequest(st,ACS,wayfURL.second,resource,NULL); + else if (!strcmp(handler->getString("wayfBinding").second,"urn:mace:shibboleth:1.0:profiles:EAuth")) { + pair localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState"); + if (!localRelayState.first || !localRelayState.second) + throw ConfigurationException("E-Authn requests cannot include relay state, so localRelayState must be enabled."); + + // Here we store the state in a cookie. + pair shib_cookie=st->getCookieNameProps("_shibstate_"); + st->setCookie(shib_cookie.first,CgiParse::url_encode(resource) + shib_cookie.second); + return make_pair(true, st->sendRedirect(wayfURL.second)); + } + else if (!XMLString::compareString(wayfBinding.second,adfs::XML::WSFED_NS)) + return ADFSAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second); + + throw UnsupportedProfileException("Unsupported WAYF binding ($1).", params(1,handler->getString("wayfBinding").second)); +} + +// Get an ACS that can handle one of the desired profiles +const IPropertySet* SessionInitiator::getCompatibleACS(const IApplication* app, const vector& profiles) +{ + // This isn't going to be very efficient until I can revise the IApplication API to + // support ACS lookup by profile. + + int mask=0; + for (vector::const_iterator p=profiles.begin(); p!=profiles.end(); p++) + mask+=*p; + + // See if the default is acceptable. + const IPropertySet* ACS=app->getDefaultAssertionConsumerService(); + pair binding=ACS ? ACS->getXMLString("Binding") : pair(false,NULL); + if (!ACS || !binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)) { + pair version = + ACS ? ACS->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol") : pair(false,1); + if (!version.first) + version.second=1; + if (mask & (version.second==1 ? SAML11_POST : SAML10_POST)) + return ACS; + } + else if (!XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) { + pair version=ACS->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol"); + if (!version.first) + version.second=1; + if (mask & (version.second==1 ? SAML11_ARTIFACT : SAML10_ARTIFACT)) + return ACS; + } + else if (!XMLString::compareString(binding.second,adfs::XML::WSFED_NS)) { + if (mask & ADFS_SSO) + return ACS; + } + + // If not, iterate by profile. + for (vector::const_iterator i=profiles.begin(); i!=profiles.end(); i++) { + for (unsigned short j=0; j<=65535; j++) { + ACS=app->getAssertionConsumerServiceByIndex(j); + if (!ACS && j) + break; // we're past 0 and didn't get a hit, so we'll bail + else if (ACS) { + binding=ACS->getXMLString("Binding"); + pair version=ACS->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol"); + if (!version.first) + version.second=1; + switch (*i) { + case SAML11_POST: + if (version.second==1 && (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST))) + return ACS; + break; + case SAML11_ARTIFACT: + if (version.second==1 && !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) + return ACS; + break; + case ADFS_SSO: + if (!XMLString::compareString(binding.second,adfs::XML::WSFED_NS)) + return ACS; + break; + case SAML10_POST: + if (version.second==0 && (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST))) + return ACS; + break; + case SAML10_ARTIFACT: + if (version.second==0 && !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) + return ACS; + break; + } + } + } + } + + return NULL; +} + +// Handles Shib 1.x AuthnRequest profile. +pair SessionInitiator::ShibAuthnRequest( + ShibTarget* st, + const IPropertySet* shire, + const char* dest, + const char* target, + const char* providerId + ) +{ + if (!shire) { + // Look for an ACS with SAML support. + vector v; + v.push_back(SAML11_POST); + v.push_back(SAML11_ARTIFACT); + v.push_back(SAML10_ARTIFACT); + v.push_back(SAML10_POST); + shire=getCompatibleACS(st->getApplication(),v); + } + if (!shire) + shire=st->getApplication()->getDefaultAssertionConsumerService(); + + // Compute the ACS URL. We add the ACS location to the handler baseURL. + // Legacy configs will not have an ACS specified, so no suffix will be added. + string ACSloc=st->getHandlerURL(target); + if (shire) ACSloc+=shire->getString("Location").second; + + char timebuf[16]; + sprintf(timebuf,"%u",time(NULL)); + string req=string(dest) + "?shire=" + CgiParse::url_encode(ACSloc.c_str()) + "&time=" + timebuf; + + // How should the resource value be preserved? + pair localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState"); + if (!localRelayState.first || !localRelayState.second) { + // The old way, just send it along. + req+="&target=" + CgiParse::url_encode(target); + } + else { + // Here we store the state in a cookie and send a fixed + // value to the IdP so we can recognize it on the way back. + pair shib_cookie=st->getCookieNameProps("_shibstate_"); + st->setCookie(shib_cookie.first,CgiParse::url_encode(target) + shib_cookie.second); + req+="&target=cookie"; + } + + // Only omitted for 1.1 style requests. + if (providerId) + req+="&providerId=" + CgiParse::url_encode(providerId); + + return make_pair(true, st->sendRedirect(req)); +} + +// Handles ADFS token request profile. +pair SessionInitiator::ADFSAuthnRequest( + ShibTarget* st, + const IPropertySet* shire, + const char* dest, + const char* target, + const char* providerId + ) +{ + if (!shire) { + // Look for an ACS with ADFS support. + vector v; + v.push_back(ADFS_SSO); + shire=getCompatibleACS(st->getApplication(),v); + } + if (!shire) + shire=st->getApplication()->getDefaultAssertionConsumerService(); + + // Compute the ACS URL. We add the ACS location to the handler baseURL. + // Legacy configs will not have an ACS specified, so no suffix will be added. + string ACSloc=st->getHandlerURL(target); + if (shire) ACSloc+=shire->getString("Location").second; + + // UTC timestamp +#ifndef HAVE_GMTIME_R + time_t epoch=time(NULL); + struct tm* ptime=gmtime(&epoch); +#else + struct tm res; + struct tm* ptime=gmtime_r(&epoch,&res); +#endif + char timebuf[32]; + strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime); + + string req=string(dest) + "?wa=wsignin1.0&wreply=" + CgiParse::url_encode(ACSloc.c_str()) + "&wct=" + CgiParse::url_encode(timebuf); + + // How should the resource value be preserved? + pair localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState"); + if (!localRelayState.first || !localRelayState.second) { + // The old way, just send it along. + req+="&wctx=" + CgiParse::url_encode(target); + } + else { + // Here we store the state in a cookie and send a fixed + // value to the IdP so we can recognize it on the way back. + pair shib_cookie=st->getCookieNameProps("_shibstate_"); + st->setCookie(shib_cookie.first,CgiParse::url_encode(target) + shib_cookie.second); + req+="&wctx=cookie"; + } + + req+="&wtrealm=" + CgiParse::url_encode(providerId); + + return make_pair(true, st->sendRedirect(req)); +} + +pair ADFSHandler::run(ShibTarget* st, const IPropertySet* handler, bool isHandler) +{ + const IApplication* app=st->getApplication(); + + if (strcasecmp(st->getRequestMethod(), "POST")) + throw FatalProfileException( + "ADFS protocol handler does not support HTTP method ($1).", params(1,st->getRequestMethod()) + ); + + if (!st->getContentType() || strcasecmp(st->getContentType(),"application/x-www-form-urlencoded")) + throw FatalProfileException( + "Blocked invalid content-type ($1) submitted to ADFS protocol handler.", params(1,st->getContentType()) + ); + + string input=st->getPostData(); + if (input.empty()) + throw FatalProfileException("ADFS protocol handler received no data from browser."); + + ShibProfile profile=ADFS_SSO; + string cookie,target,providerId; + + string hURL=st->getHandlerURL(st->getRequestURL()); + pair loc=handler->getString("Location"); + string recipient=loc.first ? hURL + loc.second : hURL; + st->getConfig()->getListener()->sessionNew( + app, + profile, + recipient.c_str(), + input.c_str(), + st->getRemoteAddr(), + target, + cookie, + providerId + ); + + st->log(ShibTarget::LogLevelDebug, string("profile processing succeeded, new session created (") + cookie + ")"); + + if (target=="default") { + pair homeURL=app->getString("homeURL"); + target=homeURL.first ? homeURL.second : "/"; + } + else if (target=="cookie" || target.empty()) { + // Pull the target value from the "relay state" cookie. + pair relay_cookie = st->getCookieNameProps("_shibstate_"); + const char* relay_state = st->getCookie(relay_cookie.first); + if (!relay_state || !*relay_state) { + // No apparent relay state value to use, so fall back on the default. + pair homeURL=app->getString("homeURL"); + target=homeURL.first ? homeURL.second : "/"; + } + else { + char* rscopy=strdup(relay_state); + CgiParse::url_decode(rscopy); + target=rscopy; + free(rscopy); + } + } + + // We've got a good session, set the session cookie. + pair shib_cookie=st->getCookieNameProps("_shibsession_"); + st->setCookie(shib_cookie.first, cookie + shib_cookie.second); + + const IPropertySet* sessionProps=app->getPropertySet("Sessions"); + pair idpHistory=sessionProps->getBool("idpHistory"); + if (!idpHistory.first || idpHistory.second) { + // Set an IdP history cookie locally (essentially just a CDC). + CommonDomainCookie cdc(st->getCookie(CommonDomainCookie::CDCName)); + + // Either leave in memory or set an expiration. + pair days=sessionProps->getUnsignedInt("idpHistoryDays"); + if (!days.first || days.second==0) + st->setCookie(CommonDomainCookie::CDCName,string(cdc.set(providerId.c_str())) + shib_cookie.second); + else { + time_t now=time(NULL) + (days.second * 24 * 60 * 60); +#ifdef HAVE_GMTIME_R + struct tm res; + struct tm* ptime=gmtime_r(&now,&res); +#else + struct tm* ptime=gmtime(&now); +#endif + char timebuf[64]; + strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime); + st->setCookie( + CommonDomainCookie::CDCName, + string(cdc.set(providerId.c_str())) + shib_cookie.second + "; expires=" + timebuf + ); + } + } + + // Now redirect to the target. + return make_pair(true, st->sendRedirect(target)); +} diff --git a/adfs/internal.h b/adfs/internal.h new file mode 100644 index 0000000..6afade2 --- /dev/null +++ b/adfs/internal.h @@ -0,0 +1,92 @@ +/* + * Copyright 2001-2005 Internet2 + * + * 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 + * + * http://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. + */ + +/* internal.h + + Scott Cantor + 2/14/04 + + $History:$ +*/ + +#ifndef __internal_h__ +#define __internal_h__ + +#include +#include +#include +#include + +#include + +#define ADFS_LOGCAT "shibtarget" +#define SHIBTRAN_LOGCAT "Shibboleth-TRANSACTION" +#define ADFS_L(s) adfs::XML::Literals::s + +namespace adfs { + + class XML + { + public: + // URI constants + static const XMLCh WSFED_NS[]; // http://schemas.xmlsoap.org/ws/2003/07/secext + static const XMLCh WSTRUST_NS[]; // http://schemas.xmlsoap.org/ws/2005/02/trust + static const XMLCh WSTRUST_SCHEMA_ID[]; + + struct Literals + { + static const XMLCh RequestedSecurityToken[]; + static const XMLCh RequestSecurityTokenResponse[]; + }; + }; + + // TODO: Publish these classes for reuse by extensions. + class CgiParse + { + public: + CgiParse(const char* data, unsigned int len); + ~CgiParse(); + const char* get_value(const char* name) const; + + static char x2c(char *what); + static void url_decode(char *url); + static std::string url_encode(const char* s); + private: + char * fmakeword(char stop, unsigned int *cl, const char** ppch); + char * makeword(char *line, char stop); + void plustospace(char *str); + + std::map kvp_map; + }; + + // Helper class for SAML 2.0 Common Domain Cookie operations + class CommonDomainCookie + { + public: + CommonDomainCookie(const char* cookie); + ~CommonDomainCookie() {} + saml::Iterator get() {return m_list;} + const char* set(const char* providerId); + static const char CDCName[]; + private: + std::string m_encoded; + std::vector m_list; + }; + + saml::SAMLAuthenticationStatement* checkAssertionProfile(const saml::SAMLAssertion* a); +} + +#endif diff --git a/adfs/listener.cpp b/adfs/listener.cpp new file mode 100644 index 0000000..3fd02dc --- /dev/null +++ b/adfs/listener.cpp @@ -0,0 +1,539 @@ +/* + * Copyright 2001-2005 Internet2 + * + * 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 + * + * http://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. + */ + +/* + * listener.cpp -- implementation of IListener functional methods that includes ADFS support + * + * Scott Cantor + * 10/10/05 + * + */ + +#include "internal.h" + +#include + +using namespace std; +using namespace log4cpp; +using namespace saml; +using namespace shibboleth; +using namespace shibtarget; +using namespace adfs; + +namespace { + class ADFSListener : public virtual IListener + { + public: + ADFSListener(const DOMElement* e) : log(&Category::getInstance(ADFS_LOGCAT".Listener")) {} + ~ADFSListener() {} + + bool create(ShibSocket& s) const {return true;} + bool bind(ShibSocket& s, bool force=false) const {return true;} + bool connect(ShibSocket& s) const {return true;} + bool close(ShibSocket& s) const {return true;} + bool accept(ShibSocket& listener, ShibSocket& s) const {return true;} + + void sessionNew( + const IApplication* application, + int supported_profiles, + const char* recipient, + const char* packet, + const char* ip, + std::string& target, + std::string& cookie, + std::string& provider_id + ) const; + + void sessionGet( + const IApplication* application, + const char* cookie, + const char* ip, + ISessionCacheEntry** pentry + ) const; + + void sessionEnd( + const IApplication* application, + const char* cookie + ) const; + + void ping(int& i) const; + + private: + Category* log; + }; +} + +IPlugIn* ADFSListenerFactory(const DOMElement* e) +{ + return new ADFSListener(e); +} + +void ADFSListener::sessionNew( + const IApplication* app, + int supported_profiles, + const char* recipient, + const char* packet, + const char* ip, + string& target, + string& cookie, + string& provider_id + ) const +{ +#ifdef _DEBUG + saml::NDC ndc("sessionNew"); +#endif + + log->debug("creating session for %s", ip); + log->debug("recipient: %s", recipient); + log->debug("application: %s", app->getId()); + + auto_ptr_XMLCh wrecipient(recipient); + + // Access the application config. It's already locked behind us. + ShibTargetConfig& stc=ShibTargetConfig::getConfig(); + IConfig* conf=stc.getINI(); + + bool checkIPAddress=true; + const IPropertySet* props=app->getPropertySet("Sessions"); + if (props) { + pair pcheck=props->getBool("checkAddress"); + if (pcheck.first) + checkIPAddress = pcheck.second; + } + + pair checkReplay=pair(false,false); + props=app->getPropertySet("Sessions"); + if (props) + checkReplay=props->getBool("checkReplay"); + + const IRoleDescriptor* role=NULL; + Metadata m(app->getMetadataProviders()); + + bool bADFS = false; + SAMLBrowserProfile::BrowserProfileResponse bpr; + + // For now, just branch off to handle ADFS inline, I'll wrap all this up later. + if (supported_profiles & ADFS_SSO) { + log->debug("executing ADFS profile..."); + CgiParse parser(packet,strlen(packet)); + const char* param=parser.get_value("wa"); + if (param && !strcmp(param,"wsignin1.0")) { + bADFS=true; + param=parser.get_value("wresult"); + if (!param) + throw FatalProfileException("ADFS profile required wresult parameter not found"); + + log->debug("decoded ADFS Token response:\n%s",param); + // wresult should carry an wst:RequestSecurityTokenResponse message so we parse it manually + DOMDocument* rdoc=NULL; + try { + saml::XML::Parser p; + static const XMLCh systemId[]={chLatin_W, chLatin_S, chDash, chLatin_T, chLatin_r, chLatin_u, chLatin_s, chLatin_t, chNull}; + MemBufInputSource membufsrc(reinterpret_cast(param),strlen(param),systemId,false); + Wrapper4InputSource dsrc(&membufsrc,false); + rdoc=p.parse(dsrc); + + // Process the wrapper and extract the assertion. + if (saml::XML::isElementNamed(rdoc->getDocumentElement(),adfs::XML::WSTRUST_NS,ADFS_L(RequestSecurityTokenResponse))) { + DOMElement* e= + saml::XML::getFirstChildElement(rdoc->getDocumentElement(),adfs::XML::WSTRUST_NS,ADFS_L(RequestedSecurityToken)); + if (e) { + e=saml::XML::getFirstChildElement(e,saml::XML::SAML_NS,L(Assertion)); + if (e) { + auto_ptr assertion(new SAMLAssertion(e)); + + // Try and map to metadata. + const IEntityDescriptor* provider=m.lookup(assertion->getIssuer()); + if (provider) + role=provider->getIDPSSODescriptor(adfs::XML::WSFED_NS); + if (!role) { + MetadataException ex("unable to locate role-specific metadata for identity provider."); + annotateException(&ex,provider); // throws it + } + + try { + // Check over the assertion. + SAMLAuthenticationStatement* authnStatement=checkAssertionProfile(assertion.get()); + + // Check signature. + log->debug("passing signed ADFS assertion to trust layer"); + Trust t(app->getTrustProviders()); + if (!t.validate(*(assertion.get()),role)) { + log->error("unable to verify signed authentication assertion"); + throw TrustException("unable to verify signed authentication assertion"); + } + + // Wrap the assertion in a dummy samlp:Response for subsequent processing. + // Generate the Response DOM using the assertion's document and then + // transfer ownership of the tree to the Response. + auto_ptr response(new SAMLResponse()); + response->addAssertion(assertion.release()); + response->toDOM(rdoc); + response->setDocument(rdoc); + rdoc=NULL; + + // Now dummy up the SAML profile response wrapper. + param=parser.get_value("wctx"); + if (param) + bpr.TARGET=param; + bpr.profile=SAMLBrowserProfile::Post; // not really, but... + bpr.response=response.release(); + bpr.assertion=response->getAssertions().next(); + bpr.authnStatement=authnStatement; + } + catch (SAMLException& ex) { + annotateException(&ex,role); // throws it + } + } + } + } + if (rdoc) { + rdoc->release(); + rdoc=NULL; + } + } + catch(...) { + if (rdoc) rdoc->release(); + throw; + } + } + if (bADFS && !bpr.response) + throw FatalProfileException("ADFS profile was indicated, but processing was unsuccesful"); + } + + // If ADFS wasn't used, proceed to SAML processing up until we reach a common point. + int minorVersion = 1; + try { + if (!bADFS) { + int allowed = 0; + if (supported_profiles & SAML11_POST || supported_profiles & SAML10_POST) + allowed |= SAMLBrowserProfile::Post; + if (supported_profiles & SAML11_ARTIFACT || supported_profiles & SAML10_ARTIFACT) + allowed |= SAMLBrowserProfile::Artifact; + minorVersion=(supported_profiles & SAML11_ARTIFACT || supported_profiles & SAML11_POST) ? 1 : 0; + + auto_ptr artifactMapper(app->getArtifactMapper()); + + // Try and run the profile. + log->debug("executing browser profile..."); + bpr=app->getBrowserProfile()->receive( + packet, + wrecipient.get(), + allowed, + (!checkReplay.first || checkReplay.second) ? conf->getReplayCache() : NULL, + artifactMapper.get(), + minorVersion + ); + + // Blow it away to clear any locks that might be held. + delete artifactMapper.release(); + + // Try and map to metadata (again). + // Once the metadata layer is in the SAML core, the repetition should be fixed. + const IEntityDescriptor* provider=m.lookup(bpr.assertion->getIssuer()); + if (!provider && bpr.authnStatement->getSubject()->getNameIdentifier() && + bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier()) + provider=m.lookup(bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier()); + if (provider) { + const IIDPSSODescriptor* IDP=provider->getIDPSSODescriptor( + minorVersion==1 ? saml::XML::SAML11_PROTOCOL_ENUM : saml::XML::SAML10_PROTOCOL_ENUM + ); + role=IDP; + } + + // This isn't likely, since the profile must have found a role. + if (!role) { + MetadataException ex("Unable to locate role-specific metadata for identity provider."); + annotateException(&ex,provider); // throws it + } + } + + // At this point, we link back up and do the same work for ADFS and SAML. + + // Maybe verify the origin address.... + if (checkIPAddress) { + log->debug("verifying client address"); + // Verify the client address exists + const XMLCh* wip = bpr.authnStatement->getSubjectIP(); + if (wip && *wip) { + // Verify the client address matches authentication + auto_ptr_char this_ip(ip); + if (strcmp(ip, this_ip.get())) { + FatalProfileException ex( + SESSION_E_ADDRESSMISMATCH, + "Your client's current address ($1) differs from the one used when you authenticated " + "to your identity provider. To correct this problem, you may need to bypass a proxy server. " + "Please contact your local support staff or help desk for assistance.", + params(1,ip) + ); + annotateException(&ex,role); // throws it + } + } + } + + // Verify condition(s) on authentication assertion. + // Attribute assertions get filtered later by the AAP. + Iterator conditions=bpr.assertion->getConditions(); + while (conditions.hasNext()) { + SAMLCondition* cond=conditions.next(); + const SAMLAudienceRestrictionCondition* ac=dynamic_cast(cond); + if (!ac) { + ostringstream os; + os << *cond; + log->error("Unrecognized Condition in authentication assertion (%s), tossing it.",os.str().c_str()); + FatalProfileException ex("unable to create session due to unrecognized condition in authentication assertion."); + annotateException(&ex,role); // throws it + } + else if (!ac->eval(app->getAudiences())) { + ostringstream os; + os << *ac; + log->error("Unacceptable AudienceRestrictionCondition in authentication assertion (%s), tossing it.",os.str().c_str()); + FatalProfileException ex("unable to create session due to unacceptable AudienceRestrictionCondition in authentication assertion."); + annotateException(&ex,role); // throws it + } + } + } + catch (SAMLException&) { + bpr.clear(); + throw; + } + catch (...) { + log->error("caught unknown exception"); + bpr.clear(); +#ifdef _DEBUG + throw; +#else + SAMLException e("An unexpected error occurred while creating your session."); + annotateException(&e,role); +#endif + } + + // It passes all our tests -- create a new session. + log->info("creating new session"); + + // Are attributes present? + bool attributesPushed=false; + Iterator assertions=bpr.response->getAssertions(); + while (!attributesPushed && assertions.hasNext()) { + Iterator statements=assertions.next()->getStatements(); + while (!attributesPushed && statements.hasNext()) { + if (dynamic_cast(statements.next())) + attributesPushed=true; + } + } + + auto_ptr_char oname(role->getEntityDescriptor()->getId()); + auto_ptr_char hname(bpr.authnStatement->getSubject()->getNameIdentifier()->getName()); + + try { + // Create a new session key. + cookie = conf->getSessionCache()->generateKey(); + + // Insert into cache. + auto_ptr as(static_cast(bpr.authnStatement->clone())); + conf->getSessionCache()->insert( + cookie.c_str(), + app, + ip, + (bADFS ? ADFS_SSO : + ((bpr.profile==SAMLBrowserProfile::Post) ? + (minorVersion==1 ? SAML11_POST : SAML10_POST) : (minorVersion==1 ? SAML11_ARTIFACT : SAML10_ARTIFACT))), + oname.get(), + as.get(), + (attributesPushed ? bpr.response : NULL), + role + ); + as.release(); // owned by cache now + } + catch (SAMLException&) { + bpr.clear(); + throw; + } + catch (...) { + log->error("caught unknown exception"); + bpr.clear(); +#ifdef _DEBUG + throw; +#else + SAMLException e("An unexpected error occurred while creating your session."); + annotateException(&e,role); +#endif + } + + target = bpr.TARGET; + provider_id = oname.get(); + + // Maybe delete the response... + if (!attributesPushed) + bpr.clear(); + + log->debug("new session id: %s", cookie.c_str()); + + // Transaction Logging + Category::getInstance(SHIBTRAN_LOGCAT).infoStream() << + "New session (ID: " << + cookie << + ") with (applicationId: " << + app->getId() << + ") for principal from (IdP: " << + provider_id << + ") at (ClientAddress: " << + ip << + ") with (NameIdentifier: " << + hname.get() << + ")"; + //stc.releaseTransactionLog(); +} + +void ADFSListener::sessionGet( + const IApplication* app, + const char* cookie, + const char* ip, + ISessionCacheEntry** pentry + ) const +{ +#ifdef _DEBUG + saml::NDC ndc("sessionGet"); +#endif + + *pentry=NULL; + log->debug("checking for session: %s@%s", cookie, ip); + + // See if the session exists... + + ShibTargetConfig& stc=ShibTargetConfig::getConfig(); + IConfig* conf=stc.getINI(); + log->debug("application: %s", app->getId()); + + bool checkIPAddress=true; + int lifetime=0,timeout=0; + const IPropertySet* props=app->getPropertySet("Sessions"); + if (props) { + pair p=props->getUnsignedInt("lifetime"); + if (p.first) + lifetime = p.second; + p=props->getUnsignedInt("timeout"); + if (p.first) + timeout = p.second; + pair pcheck=props->getBool("checkAddress"); + if (pcheck.first) + checkIPAddress = pcheck.second; + } + + *pentry = conf->getSessionCache()->find(cookie,app); + + // If not, leave now.. + if (!*pentry) { + log->debug("session not found"); + throw InvalidSessionException("No session exists for key value ($session_id)",namedparams(1,"session_id",cookie)); + } + + // TEST the session... + try { + // Verify the address is the same + if (checkIPAddress) { + log->debug("Checking address against %s", (*pentry)->getClientAddress()); + if (strcmp(ip, (*pentry)->getClientAddress())) { + log->debug("client address mismatch"); + InvalidSessionException ex( + SESSION_E_ADDRESSMISMATCH, + "Your IP address (%1) does not match the address recorded at the time the session was established.", + params(1,ip) + ); + Metadata m(app->getMetadataProviders()); + annotateException(&ex,m.lookup((*pentry)->getProviderId())); // throws it + } + } + + // and that the session is still valid... + if (!(*pentry)->isValid(lifetime,timeout)) { + log->debug("session expired"); + InvalidSessionException ex(SESSION_E_EXPIRED, "Your session has expired, and you must re-authenticate."); + Metadata m(app->getMetadataProviders()); + annotateException(&ex,m.lookup((*pentry)->getProviderId())); // throws it + } + } + catch (SAMLException&) { + (*pentry)->unlock(); + *pentry=NULL; + conf->getSessionCache()->remove(cookie); + + // Transaction Logging + Category::getInstance(SHIBTRAN_LOGCAT).infoStream() << + "Destroyed invalid session (ID: " << + cookie << + ") with (applicationId: " << + app->getId() << + "), request was from (ClientAddress: " << + ip << + ")"; + //stc.releaseTransactionLog(); + throw; + } + catch (...) { + log->error("caught unknown exception"); +#ifndef _DEBUG + InvalidSessionException ex("An unexpected error occurred while validating your session, and you must re-authenticate."); + Metadata m(app->getMetadataProviders()); + annotateException(&ex,m.lookup((*pentry)->getProviderId()),false); +#endif + (*pentry)->unlock(); + *pentry=NULL; + conf->getSessionCache()->remove(cookie); + + // Transaction Logging + Category::getInstance(SHIBTRAN_LOGCAT).infoStream() << + "Destroyed invalid session (ID: " << + cookie << + ") with (applicationId: " << + app->getId() << + "), request was from (ClientAddress: " << + ip << + ")"; + //stc.releaseTransactionLog(); +#ifdef _DEBUG + throw; +#else + ex.raise(); +#endif + } + + log->debug("session ok"); +} + +void ADFSListener::sessionEnd( + const IApplication* application, + const char* cookie + ) const +{ +#ifdef _DEBUG + saml::NDC ndc("sessionEnd"); +#endif + + log->debug("removing session: %s", cookie); + + ShibTargetConfig& stc=ShibTargetConfig::getConfig(); + stc.getINI()->getSessionCache()->remove(cookie); + + // Transaction Logging + Category::getInstance(SHIBTRAN_LOGCAT).infoStream() << "Destroyed session (ID: " << cookie << ")"; + //stc.releaseTransactionLog(); +} + +void ADFSListener::ping(int& i) const +{ + i++; +} diff --git a/adfs/resource.h b/adfs/resource.h new file mode 100644 index 0000000..3ba1cce --- /dev/null +++ b/adfs/resource.h @@ -0,0 +1,15 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Developer Studio generated include file. +// Used by adfs.rc +// + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1000 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif -- 2.1.4