diff --git a/.gitignore b/.gitignore index 29627cd..6ff29bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,366 @@ -source-code/obj/* -source-code/bin/* -source-code/.vs/* +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo *.user -samltestapp4*.* \ No newline at end of file +*.userosscache +*.sln.docstates +source-code/.config +.vscode +.idea +.DS_Store + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +source-code/[Dd]ebug/ +source-code/[Dd]ebugPublic/ +source-code/[Rr]elease/ +source-code/[Rr]eleases/ +source-code/x64/ +source-code/x86/ +source-code/[Ww][Ii][Nn]32/ +source-code/[Aa][Rr][Mm]/ +source-code/[Aa][Rr][Mm]64/ +source-code/bld/ +source-code/[Bb]in/ +source-code/[Oo]bj/ +source-code/[Ll]og/ +source-code/[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/source-code/Models/AzureAdB2C.cs b/source-code/Models/AzureAdB2C.cs new file mode 100644 index 0000000..24bfd1c --- /dev/null +++ b/source-code/Models/AzureAdB2C.cs @@ -0,0 +1,10 @@ +namespace SAMLTEST.Models; +public class AzureAdB2C +{ + public static string ConfigurationName => nameof(AzureAdB2C); + public string Tenant { get; set; } + public string HostName { get; set; } + public string Policy { get; set; } + public string Issuer { get; set; } + public string DCInfo { get; set; } +} \ No newline at end of file diff --git a/source-code/Pages/IDP/AuthNRequest.cshtml.cs b/source-code/Pages/IDP/AuthNRequest.cshtml.cs index 0469355..be37a89 100755 --- a/source-code/Pages/IDP/AuthNRequest.cshtml.cs +++ b/source-code/Pages/IDP/AuthNRequest.cshtml.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Configuration; using SAMLTEST.SAMLObjects; using System; @@ -14,9 +13,9 @@ namespace SAMLTEST.Pages.IDP /// public class AuthNRequestModel : PageModel { - public String RelayState { get; set; } - public String ACS { get; set; } - public String ID { get; private set; } + public string RelayState { get; set; } + public string ACS { get; set; } + public string ID { get; private set; } [DisplayName("UserName")] public string UserName { get; set; } [DisplayName("Password")] @@ -34,16 +33,16 @@ public AuthNRequestModel(IConfiguration configuration) } /// - /// This Get Action is used to Generate and POST the SAML Repsonse + /// This Get Action is used to Generate and POST the SAML Response /// based on a supplied AuthN Request /// - public void OnGet(String SAMLRequest, String RelayState) - { + public void OnGet(string SAMLRequest, string RelayState) + { this.RelayState = RelayState; - String sml = SAMLHelper.Decompress(SAMLRequest); - XmlDocument doc = new XmlDocument(); - XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable); + var sml = SAMLHelper.Decompress(SAMLRequest); + var doc = new XmlDocument(); + var nsmgr = new XmlNamespaceManager(doc.NameTable); nsmgr.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion"); nsmgr.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol"); doc.LoadXml(sml); @@ -52,10 +51,10 @@ public void OnGet(String SAMLRequest, String RelayState) ACS = root.SelectSingleNode("/samlp:AuthnRequest/@AssertionConsumerServiceURL", nsmgr).Value; ID = root.SelectSingleNode("/samlp:AuthnRequest/@ID", nsmgr).Value; - string httpors = HttpContext.Request.IsHttps ? "https://" : "http://"; - string thisurl = httpors + HttpContext.Request.Host.Value; - SAMLResponse Resp = new SAMLResponse(ACS, ID, thisurl, _configuration); - this.SAMLResponse = Convert.ToBase64String(Encoding.UTF8.GetBytes(Resp. ToString())); + var httpors = HttpContext.Request.IsHttps ? "https://" : "http://"; + var thisurl = httpors + HttpContext.Request.Host.Value; + var response = new SAMLResponse(ACS, ID, thisurl, _configuration); + this.SAMLResponse = Convert.ToBase64String(Encoding.UTF8.GetBytes(response.ToString())); this.RelayState = RelayState; } diff --git a/source-code/Pages/IDP/Index.cshtml b/source-code/Pages/IDP/Index.cshtml index 807cc60..ab32148 100755 --- a/source-code/Pages/IDP/Index.cshtml +++ b/source-code/Pages/IDP/Index.cshtml @@ -25,11 +25,12 @@ The Policy name with or without B2C_1A_
@Html.ValidationMessageFor(m => m.Policy)
+
+ @Html.LabelFor(m => m.Issuer) + @Html.TextBoxFor(m => m.Issuer, htmlAttributes: new { @class = "form-control", @placeholder = "Enter Entity ID a.k.a app identifier uri" }) + The App Identifier URI (from application overview page) +
@Html.ValidationMessageFor(m => m.Issuer)
+
-} - - - - - +} \ No newline at end of file diff --git a/source-code/Pages/IDP/Index.cshtml.cs b/source-code/Pages/IDP/Index.cshtml.cs index 9535b4b..75eb55b 100755 --- a/source-code/Pages/IDP/Index.cshtml.cs +++ b/source-code/Pages/IDP/Index.cshtml.cs @@ -1,6 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Configuration; +using SAMLTEST.Models; using SAMLTEST.SAMLObjects; using System; using System.ComponentModel; @@ -17,8 +19,9 @@ public class IndexModel : PageModel [DisplayName("Tenant Name"), Required] public string Tenant { get; set; } [DisplayName("B2C Policy"), Required] - public string Policy { get; set; } = "SAMLTEST"; - + public string Policy { get; set; } + [DisplayName("Issuer"), Required] + public string Issuer { get; set; } private readonly IConfiguration _configuration; /// @@ -27,27 +30,57 @@ public class IndexModel : PageModel public IndexModel(IConfiguration configuration) { _configuration = configuration; + var azureAdB2C = new AzureAdB2C(); + configuration.GetSection(AzureAdB2C.ConfigurationName).Bind(azureAdB2C); + Tenant = azureAdB2C.Tenant; + Policy = azureAdB2C.Policy; + Issuer = azureAdB2C.Issuer; + } + + public IActionResult OnGet(string tenant, string policy, string issuer) + { + // Try to get values from the sessions, if none then use the default values + this.Tenant = string.IsNullOrEmpty(HttpContext.Session.GetString("Tenant")) ? this.Tenant : HttpContext.Session.GetString("Tenant"); + this.Policy = string.IsNullOrEmpty(HttpContext.Session.GetString("Policy")) ? this.Policy : HttpContext.Session.GetString("Policy"); + this.Issuer = string.IsNullOrEmpty(HttpContext.Session.GetString("Issuer")) ? this.Issuer : HttpContext.Session.GetString("Issuer"); + // Override the values with the query string values + if (!string.IsNullOrEmpty(tenant)) + { + this.Tenant = tenant.Contains("onmicrosoft.com", StringComparison.OrdinalIgnoreCase) ? tenant : $"{tenant}.onmicrosoft.com"; ; + } + if (!string.IsNullOrEmpty(policy)) + { + this.Policy = policy.StartsWith("B2C_1A_") ? policy : "B2C_1A_" + policy; ; + } + if (!string.IsNullOrEmpty(issuer)) + { + this.Issuer = issuer; + } + // Save the values to the session for the future use + if (null != this.Tenant) HttpContext.Session.SetString("Tenant", this.Tenant); + if (null != this.Policy) HttpContext.Session.SetString("Policy", this.Policy); + if (null != this.Issuer) HttpContext.Session.SetString("Issuer", this.Issuer); + return Page(); } /// /// This Post Action is used to Generate and POST the SAML Repsonse for and IDP initiated SSO /// - public IActionResult OnPost(string Tenant, string Policy) + public IActionResult OnPost(string tenant, string policy) { - - string b2cloginurl = _configuration["SAMLTEST:b2cloginurl"]; - Policy = Policy.StartsWith("B2C_1A_") ? Policy : "B2C_1A_" + Policy; - Tenant = Tenant.ToLower().Contains("onmicrosoft.com") ? Tenant : Tenant + ".onmicrosoft.com"; - - - string ACS = "https://" + b2cloginurl + "/te/" + Tenant + "/" + Policy + "/samlp/sso/assertionconsumer"; + Policy = policy.StartsWith("B2C_1A_") ? policy : "B2C_1A_" + policy; + Tenant = tenant.Contains("onmicrosoft.com", StringComparison.OrdinalIgnoreCase) ? tenant : $"{tenant}.onmicrosoft.com"; + var b2cLoginDomain = $"{Tenant.Split(".")[0]}.b2clogin.com"; + var relayState = $"{SAMLHelper.toB64(Tenant)}.{SAMLHelper.toB64(Policy)}.{SAMLHelper.toB64(Issuer)}"; + // To sign in or sign up a user through IdP-initiated flow, use the following URL: + // https://.b2clogin.com/.onmicrosoft.com//generic/login?EntityId=&RelayState= + // source: https://learn.microsoft.com/en-us/azure/active-directory-b2c/saml-service-provider-options?pivots=b2c-custom-policy#configure-idp-initiated-flow + var ACS = $"https://{b2cLoginDomain}/{Tenant}/{Policy}/generic/login?EntityId={Issuer}&RelayState={System.Web.HttpUtility.UrlEncode(relayState)}"; SAMLResponse Resp = new SAMLResponse(ACS, "", SAMLHelper.GetThisURL(this), _configuration); - string SAMLResponse = Convert.ToBase64String(Encoding.UTF8.GetBytes(Resp.ToString())); - - return Content(SAMLHelper.GeneratePost(SAMLResponse,ACS,"SAMLResponse"),"text/html"); - + var SAMLResponse = Convert.ToBase64String(Encoding.UTF8.GetBytes(Resp.ToString())); + return Content(SAMLHelper.GeneratePost(SAMLResponse, ACS, "SAMLResponse"), "text/html"); } } } \ No newline at end of file diff --git a/source-code/Pages/Logout.cshtml.cs b/source-code/Pages/Logout.cshtml.cs index 547e941..92d1a39 100755 --- a/source-code/Pages/Logout.cshtml.cs +++ b/source-code/Pages/Logout.cshtml.cs @@ -1,15 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.IO; -using System.Reflection; -using SAMLTEST.SAMLObjects; using Microsoft.Extensions.Configuration; +using SAMLTEST.SAMLObjects; +using System; +using System.ComponentModel; namespace SAMLTEST.Pages { @@ -18,7 +12,7 @@ public class LogoutModel : PageModel private readonly IConfiguration _configuration; [DisplayName("Tenant Name")] - public string Tenant { get; set; } + public string Tenant { get; set; } [DisplayName("B2C Policy")] public string Policy { get; set; } @@ -37,40 +31,32 @@ public LogoutModel(IConfiguration configuration) } - public IActionResult OnGet(string Tenant, string Policy, string SessionId,string NameId, string Issuer, string DCInfo) + public IActionResult OnGet(string tenant, string policy, string sessionId, string nameId, string issuer, string dcInfo) { - if (String.IsNullOrEmpty(Tenant) || String.IsNullOrEmpty(Policy)) - return Page(); - else - return OnPost(Tenant, Policy, SessionId, NameId, Issuer, DCInfo); + return (string.IsNullOrEmpty(tenant) || String.IsNullOrEmpty(policy)) + ? Page() + : OnPost(tenant, policy, sessionId, nameId, issuer, dcInfo); } - public IActionResult OnPost(string Tenant, string Policy, string SessionId, string NameId, string Issuer, string DCInfo) + public IActionResult OnPost(string tenant, string policy, string sessionId, string nameId, string issuer, string dcInfo) { - string b2cloginurl = "";// Tenant.Split('.')[0] + ".b2clogin.com"; - if ( !Tenant.EndsWith(".onmicrosoft.com")) - { - b2cloginurl = Tenant; - } else - { - b2cloginurl = Tenant.Split('.')[0] + ".b2clogin.com"; - } + Policy = policy.StartsWith("B2C_1A_") ? policy : "B2C_1A_" + policy; + Tenant = tenant.Contains("onmicrosoft.com", StringComparison.OrdinalIgnoreCase) ? tenant : $"{tenant}.onmicrosoft.com"; + Issuer = string.IsNullOrEmpty(issuer) ? this.Issuer : issuer; + var b2cLoginDomain = $"{Tenant.Split(".")[0]}.b2clogin.com"; + var relayState = $"{SAMLHelper.toB64(Tenant)}.{SAMLHelper.toB64(Policy)}.{SAMLHelper.toB64(Issuer)}"; - if (!string.IsNullOrEmpty(DCInfo)) + if (!string.IsNullOrEmpty(dcInfo)) { - DCInfo = DCInfo.Replace("dc", "&dc"); - DCInfo = DCInfo.Replace("slice", "&slice"); + DCInfo = dcInfo.Replace("dc", "&dc").Replace("slice", "&slice"); } - Policy = Policy.StartsWith("B2C_1A_") ? Policy : "B2C_1A_" + Policy; - Tenant = (Tenant.ToLower().Contains("onmicrosoft.com") || Tenant.ToLower().Contains("ccsctp.net")) ? Tenant : Tenant + ".onmicrosoft.com"; - string URL = "https://" + b2cloginurl + "/te/" + Tenant + "/" + Policy + "/samlp/sso/logout?" + DCInfo; + var url = $"https://{b2cLoginDomain}/{Tenant}/{Policy}/samlp/sso/logout?{DCInfo}"; + var logoutRequest = new LogoutRequest(url, SAMLHelper.GetThisURL(this), sessionId, nameId, Issuer); + var cdoc = SAMLHelper.Compress(logoutRequest.ToString()); + url = $"{url}&SAMLRequest={System.Web.HttpUtility.UrlEncode(cdoc)}"; - LogoutRequest logoutRequest = new LogoutRequest(URL, SAMLHelper.GetThisURL(this), SessionId, NameId, Issuer); - string cdoc = SAMLHelper.Compress(logoutRequest.ToString()); - URL = URL + "&SAMLRequest=" + System.Web.HttpUtility.UrlEncode(cdoc); - - return Redirect(URL); + return Redirect(url); } } diff --git a/source-code/Pages/Metadata.cshtml.cs b/source-code/Pages/Metadata.cshtml.cs index 38588b4..566c83b 100755 --- a/source-code/Pages/Metadata.cshtml.cs +++ b/source-code/Pages/Metadata.cshtml.cs @@ -12,7 +12,7 @@ public class MetadataModel : PageModel public string ServerName { get; private set; } public Boolean ShowView { get; private set; } = false; - public void OnGet(string showpage="false") + public void OnGet(string showpage = "false") { ServerName = SAMLHelper.GetThisURL(this); if (showpage != "false") diff --git a/source-code/Pages/SP/AssertionConsumer.cshtml b/source-code/Pages/SP/AssertionConsumer.cshtml index 5f45b34..9e637f2 100755 --- a/source-code/Pages/SP/AssertionConsumer.cshtml +++ b/source-code/Pages/SP/AssertionConsumer.cshtml @@ -16,7 +16,7 @@ - @foreach (KeyValuePair element in Model.attrsandvals) + @foreach (KeyValuePair element in Model.Attrsandvals) { diff --git a/source-code/Pages/SP/AssertionConsumer.cshtml.cs b/source-code/Pages/SP/AssertionConsumer.cshtml.cs index d1570b1..04fb61f 100755 --- a/source-code/Pages/SP/AssertionConsumer.cshtml.cs +++ b/source-code/Pages/SP/AssertionConsumer.cshtml.cs @@ -11,29 +11,29 @@ namespace SAMLTEST.Pages.SP /// /// This is the Assertion Consumer Page Model /// This page will be posted to from outside this application - /// thuys the Ignore Anti Forgery Token below + /// thus the Ignore Anti Forgery Token below /// [IgnoreAntiforgeryToken(Order = 1001)] public class AssertionConsumerModel : PageModel { - public String SessionId { get; private set; } + public string SessionId { get; private set; } - public String SAMLResponse { get; private set; } - public Dictionary attrsandvals { get; private set; } + public string SAMLResponse { get; private set; } + public Dictionary Attrsandvals { get; private set; } - public String TenantId { get; private set; } - public String PolicyId { get; private set; } - public String NameId { get; private set; } + public string TenantId { get; private set; } + public string PolicyId { get; private set; } + public string NameId { get; private set; } - public String DCInfo { get; private set; } - public String Issuer { get; private set; } + public string DCInfo { get; private set; } + public string Issuer { get; private set; } - public IActionResult OnPost(string SAMLResponse, string RelayState) + public IActionResult OnPost(string samlResponse, string relayState) { //Get Tenant, Policy, Issuer and DCInfo from RelayState - if (!String.IsNullOrWhiteSpace(RelayState)) + if (!string.IsNullOrWhiteSpace(relayState)) { - string[] RelayStateBits = RelayState.Split("."); + string[] RelayStateBits = relayState.Split("."); this.TenantId = SAMLHelper.fromB64(RelayStateBits[0]); this.PolicyId = SAMLHelper.fromB64(RelayStateBits[1]); this.Issuer = SAMLHelper.fromB64(RelayStateBits[2]); @@ -48,10 +48,10 @@ public IActionResult OnPost(string SAMLResponse, string RelayState) } } - byte[] ENcSAMLByteArray = Convert.FromBase64String(SAMLResponse); - String sml = System.Text.ASCIIEncoding.ASCII.GetString(ENcSAMLByteArray); - XmlDocument doc = new XmlDocument(); - XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable); + byte[] ENcSAMLByteArray = Convert.FromBase64String(samlResponse); + var sml = System.Text.ASCIIEncoding.ASCII.GetString(ENcSAMLByteArray); + var doc = new XmlDocument(); + var nsmgr = new XmlNamespaceManager(doc.NameTable); nsmgr.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion"); nsmgr.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol"); doc.LoadXml(sml); @@ -64,30 +64,31 @@ public IActionResult OnPost(string SAMLResponse, string RelayState) return Redirect("/Error?ErrorMessage=" + statusMessage); } - XmlNodeList nodes = root.SelectNodes("/samlp:Response/saml:Assertion/saml:AttributeStatement/saml:Attribute", nsmgr); - this.attrsandvals = new Dictionary(); + XmlNodeList nodes = root.SelectNodes("/samlp:Response/saml:Assertion/saml:AttributeStatement/saml:Attribute", nsmgr); + this.Attrsandvals = new Dictionary(); foreach (XmlNode node in nodes) { - String attrname = node.Attributes["Name"].Value; - String val = ""; + var attributeName = node.Attributes["Name"].Value; + var val = string.Empty; if (node.HasChildNodes && node.ChildNodes.Count > 1) { var values = node.ChildNodes.Cast() - .Select(item => item.InnerText).ToList(); + .Select(item => item.InnerText) + .ToList(); val = string.Join("
", values); } else { val = node.InnerText; } - this.attrsandvals.Add(attrname, val); + this.Attrsandvals.Add(attributeName, val); } this.SAMLResponse = sml; this.SessionId = root.SelectSingleNode("/samlp:Response/saml:Assertion/saml:AuthnStatement/@SessionIndex", nsmgr).Value; this.NameId = root.SelectSingleNode("/samlp:Response/saml:Assertion/saml:Subject/saml:NameID", nsmgr).InnerText; return Page(); - + } } } \ No newline at end of file diff --git a/source-code/Pages/SP/Index.cshtml.cs b/source-code/Pages/SP/Index.cshtml.cs index 5255af5..c240f61 100755 --- a/source-code/Pages/SP/Index.cshtml.cs +++ b/source-code/Pages/SP/Index.cshtml.cs @@ -1,7 +1,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using SAMLTEST.Models; using SAMLTEST.SAMLObjects; using System; using System.ComponentModel; @@ -19,112 +20,124 @@ public class IndexModel : PageModel [DisplayName("Tenant Name"), Required] - public string Tenant { get; set; } = "azureadb2ctests"; + public string Tenant { get; set; } [DisplayName("Host Name"), Required] - public string HostName { get; set; } = "azureadb2ctests.b2clogin.com"; + public string HostName { get; set; } - [DisplayName("B2C Policy"),Required] - public string Policy { get; set; } = "B2C_1A_SignUpOrSignin_SamlApp_Local"; - + [DisplayName("B2C Policy"), Required] + public string Policy { get; set; } [DisplayName("Issuer")] - public string Issuer { get; set; } = "https://azureadb2ctests.onmicrosoft.com/samlAPPUITest"; + public string Issuer { get; set; } [DisplayName("DCInfo")] - public string DCInfo { get; set; } = ""; + public string DCInfo { get; set; } - private readonly IConfiguration _configuration; + private readonly AzureAdB2C _azureAdB2C; /// /// This Constructor is used to retrieve the Appsettings data /// - public IndexModel(IConfiguration configuration) + public IndexModel(IOptions options) { - _configuration = configuration; + _azureAdB2C = options.Value; + Tenant = _azureAdB2C.Tenant; + HostName = _azureAdB2C.HostName; + Policy = _azureAdB2C.Policy; + Issuer = _azureAdB2C.Issuer; + DCInfo = _azureAdB2C.DCInfo; } - public IActionResult OnGet(string Tenant, string HostName, string Policy, string Issuer) + public IActionResult OnGet(string tenant, string hostName, string policy, string issuer) { - this.Tenant = HttpContext.Session.GetString("Tenant"); - this.HostName = HttpContext.Session.GetString("HostName"); - this.Policy = HttpContext.Session.GetString("Policy"); - this.Issuer = HttpContext.Session.GetString("Issuer"); - if (!string.IsNullOrEmpty(Tenant)) { - this.Tenant = Tenant; + // Try to get values from the sessions, if none then use the default values + this.Tenant = string.IsNullOrEmpty(HttpContext.Session.GetString("Tenant")) ? this.Tenant : HttpContext.Session.GetString("Tenant"); + this.HostName = string.IsNullOrEmpty(HttpContext.Session.GetString("HostName")) ? this.HostName : HttpContext.Session.GetString("HostName"); + this.Policy = string.IsNullOrEmpty(HttpContext.Session.GetString("Policy")) ? this.Policy : HttpContext.Session.GetString("Policy"); + this.Issuer = string.IsNullOrEmpty(HttpContext.Session.GetString("Issuer")) ? this.Issuer : HttpContext.Session.GetString("Issuer"); + // Override the values with the query string values + if (!string.IsNullOrEmpty(tenant)) + { + this.Tenant = tenant; } - if (!string.IsNullOrEmpty(HostName)) + if (!string.IsNullOrEmpty(hostName)) { - this.HostName = HostName; + this.HostName = hostName; } // if still null, build up hostname yourtenant.b2clogin.com from tenant name yourtenant.onmicrosoft.com - if (string.IsNullOrEmpty(this.HostName) ) + if (string.IsNullOrEmpty(this.HostName)) { string TenantName = this.Tenant.ToLower()?.Replace(".onmicrosoft.com", ""); this.HostName = TenantName + ".b2clogin.com"; } - if (!string.IsNullOrEmpty(Policy)) { - this.Policy = Policy; + if (!string.IsNullOrEmpty(policy)) + { + this.Policy = policy; } - if (!string.IsNullOrEmpty(Issuer)) { - this.Issuer = Issuer; + if (!string.IsNullOrEmpty(issuer)) + { + this.Issuer = issuer; } - if ( null != this.Tenant) HttpContext.Session.SetString("Tenant", this.Tenant); - if ( null != this.HostName) HttpContext.Session.SetString("HostName", this.HostName); - if ( null != this.Policy) HttpContext.Session.SetString("Policy", this.Policy); - if ( null != this.Issuer ) HttpContext.Session.SetString("Issuer", this.Issuer); + // Save the values to the session for the future use + if (null != this.Tenant) HttpContext.Session.SetString("Tenant", this.Tenant); + if (null != this.HostName) HttpContext.Session.SetString("HostName", this.HostName); + if (null != this.Policy) HttpContext.Session.SetString("Policy", this.Policy); + if (null != this.Issuer) HttpContext.Session.SetString("Issuer", this.Issuer); return Page(); } /// /// This Post Action is used to Generate the AuthN Request and redirect to the B2C Login endpoint /// - public IActionResult OnPost(string Tenant, string HostName, string Policy, string Issuer, string DCInfo, bool IsAzureAD) + public IActionResult OnPost(string tenant, string hostName, string policy, string issuer, string dcInfo, bool isAzureAD) { - if (string.IsNullOrEmpty(Policy) || IsAzureAD) + if (string.IsNullOrEmpty(policy) || isAzureAD) { - return SendAzureAdRequest(Tenant); + return SendAzureAdRequest(tenant); } - string SamlRequest = string.Empty; - string b2cloginurl = HostName.ToLower(); - if (!String.IsNullOrEmpty(HostName)) + string b2cloginurl = hostName.ToLower(); + if (!string.IsNullOrEmpty(hostName)) { - b2cloginurl = HostName; + b2cloginurl = hostName; } - else if (!String.IsNullOrEmpty(this.Tenant) && this.Tenant.EndsWith(".onmicrosoft.com")) + else if (!string.IsNullOrEmpty(this.Tenant) && this.Tenant.EndsWith(".onmicrosoft.com")) { - string TenantName = Tenant.ToLower()?.Replace(".onmicrosoft.com", ""); + string TenantName = tenant.ToLower()?.Replace(".onmicrosoft.com", ""); b2cloginurl = TenantName + ".b2clogin.com"; } - Policy = Policy.StartsWith("B2C_1A_") ? Policy : "B2C_1A_" + Policy; - //Tenant = (Tenant.ToLower().Contains("onmicrosoft.com") || Tenant.ToLower().Contains(".net")) ? Tenant : Tenant + ".onmicrosoft.com"; - DCInfo = string.IsNullOrWhiteSpace(DCInfo) ? string.Empty : "&" + DCInfo; - Issuer = string.IsNullOrWhiteSpace(Issuer) ? SAMLHelper.GetThisURL(this) : Issuer; + policy = policy.StartsWith("B2C_1A_") ? policy : "B2C_1A_" + policy; + tenant = (tenant.Contains("onmicrosoft.com", StringComparison.OrdinalIgnoreCase) + || tenant.Contains(".net", StringComparison.OrdinalIgnoreCase)) + ? tenant + : $"{tenant}.onmicrosoft.com"; + dcInfo = string.IsNullOrWhiteSpace(dcInfo) ? string.Empty : $"&{dcInfo}"; + issuer = string.IsNullOrWhiteSpace(issuer) ? SAMLHelper.GetThisURL(this) : issuer; - if (null != Tenant) HttpContext.Session.SetString("Tenant", Tenant); + if (null != tenant) HttpContext.Session.SetString("Tenant", tenant); if (null != b2cloginurl) HttpContext.Session.SetString("HostName", b2cloginurl); - if (null != Policy) HttpContext.Session.SetString("Policy", Policy); - if (null != Issuer) HttpContext.Session.SetString("Issuer", Issuer); + if (null != policy) HttpContext.Session.SetString("Policy", policy); + if (null != issuer) HttpContext.Session.SetString("Issuer", issuer); - string RelayState = SAMLHelper.toB64(Tenant) + "." + SAMLHelper.toB64(Policy) + "." + SAMLHelper.toB64(Issuer); + string RelayState = $"{SAMLHelper.toB64(tenant)}.{SAMLHelper.toB64(policy)}.{SAMLHelper.toB64(issuer)}"; - if (!string.IsNullOrEmpty(DCInfo)) + if (!string.IsNullOrEmpty(dcInfo)) { - RelayState = RelayState + "." + SAMLHelper.toB64(DCInfo); + RelayState = $"{RelayState}.{SAMLHelper.toB64(dcInfo)}"; } AuthnRequest AuthnReq; - string URL = "https://" + b2cloginurl + "/" + Tenant + "/" + Policy + "/samlp/sso/login?" + DCInfo; - AuthnReq = new AuthnRequest(URL, SAMLHelper.GetThisURL(this), Issuer); + string URL = $"https://{b2cloginurl}/{tenant}/{policy}/samlp/sso/login?{dcInfo}"; + AuthnReq = new AuthnRequest(URL, SAMLHelper.GetThisURL(this), issuer); string cdoc = SAMLHelper.Compress(AuthnReq.ToString()); - URL = URL + "&SAMLRequest=" + System.Web.HttpUtility.UrlEncode(cdoc) + "&RelayState=" + System.Web.HttpUtility.UrlEncode(RelayState); + URL = $"{URL}&SAMLRequest={System.Web.HttpUtility.UrlEncode(cdoc)}&RelayState={System.Web.HttpUtility.UrlEncode(RelayState)}"; return Redirect(URL); } - public IActionResult SendAzureAdRequest(string Tenant) + public IActionResult SendAzureAdRequest(string tenant) { AuthnRequest AuthnReq; AuthnReq = new AuthnRequest("https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/saml2", SAMLHelper.GetThisURL(this), string.Empty); diff --git a/source-code/Pages/_Layout.cshtml b/source-code/Pages/_Layout.cshtml index 5ccbca3..9e5a074 100755 --- a/source-code/Pages/_Layout.cshtml +++ b/source-code/Pages/_Layout.cshtml @@ -30,7 +30,7 @@