├── .github
└── workflows
│ └── dotnet.yml
├── .gitignore
├── AspNetSaml.Tests
├── AspNetSaml.Tests.csproj
├── UnitTests.cs
└── Usings.cs
├── AspNetSaml.WebExample
├── AspNetSaml.WebExample.csproj
├── Program.cs
├── Properties
│ └── launchSettings.json
├── README
└── appsettings.json
├── AspNetSaml.sln
├── AspNetSaml
├── AspNetSaml.csproj
└── Saml.cs
├── LICENSE
├── README.md
└── testenvironments.json
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a .NET project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
3 |
4 | name: .NET
5 |
6 | on:
7 | push:
8 | branches: [ "master" ]
9 | pull_request:
10 | branches: [ "master" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Setup .NET
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: 8.0.x
23 | - name: Restore dependencies
24 | run: dotnet restore
25 | - name: Build
26 | run: dotnet build --no-restore
27 | - name: Test
28 | run: dotnet test --no-build --verbosity normal
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # NCrunch
145 | _NCrunch_*
146 | .*crunch*.local.xml
147 | nCrunchTemp_*
148 |
149 | # MightyMoose
150 | *.mm.*
151 | AutoTest.Net/
152 |
153 | # Web workbench (sass)
154 | .sass-cache/
155 |
156 | # Installshield output folder
157 | [Ee]xpress/
158 |
159 | # DocProject is a documentation generator add-in
160 | DocProject/buildhelp/
161 | DocProject/Help/*.HxT
162 | DocProject/Help/*.HxC
163 | DocProject/Help/*.hhc
164 | DocProject/Help/*.hhk
165 | DocProject/Help/*.hhp
166 | DocProject/Help/Html2
167 | DocProject/Help/html
168 |
169 | # Click-Once directory
170 | publish/
171 |
172 | # Publish Web Output
173 | *.[Pp]ublish.xml
174 | *.azurePubxml
175 | # Note: Comment the next line if you want to checkin your web deploy settings,
176 | # but database connection strings (with potential passwords) will be unencrypted
177 | *.pubxml
178 | *.publishproj
179 |
180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
181 | # checkin your Azure Web App publish settings, but sensitive information contained
182 | # in these scripts will be unencrypted
183 | PublishScripts/
184 |
185 | # NuGet Packages
186 | *.nupkg
187 | # NuGet Symbol Packages
188 | *.snupkg
189 | # The packages folder can be ignored because of Package Restore
190 | **/[Pp]ackages/*
191 | # except build/, which is used as an MSBuild target.
192 | !**/[Pp]ackages/build/
193 | # Uncomment if necessary however generally it will be regenerated when needed
194 | #!**/[Pp]ackages/repositories.config
195 | # NuGet v3's project.json files produces more ignorable files
196 | *.nuget.props
197 | *.nuget.targets
198 |
199 | # Microsoft Azure Build Output
200 | csx/
201 | *.build.csdef
202 |
203 | # Microsoft Azure Emulator
204 | ecf/
205 | rcf/
206 |
207 | # Windows Store app package directories and files
208 | AppPackages/
209 | BundleArtifacts/
210 | Package.StoreAssociation.xml
211 | _pkginfo.txt
212 | *.appx
213 | *.appxbundle
214 | *.appxupload
215 |
216 | # Visual Studio cache files
217 | # files ending in .cache can be ignored
218 | *.[Cc]ache
219 | # but keep track of directories ending in .cache
220 | !?*.[Cc]ache/
221 |
222 | # Others
223 | ClientBin/
224 | ~$*
225 | *~
226 | *.dbmdl
227 | *.dbproj.schemaview
228 | *.jfm
229 | *.pfx
230 | *.publishsettings
231 | orleans.codegen.cs
232 |
233 | # Including strong name files can present a security risk
234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
235 | #*.snk
236 |
237 | # Since there are multiple workflows, uncomment next line to ignore bower_components
238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
239 | #bower_components/
240 |
241 | # RIA/Silverlight projects
242 | Generated_Code/
243 |
244 | # Backup & report files from converting an old project file
245 | # to a newer Visual Studio version. Backup files are not needed,
246 | # because we have git ;-)
247 | _UpgradeReport_Files/
248 | Backup*/
249 | UpgradeLog*.XML
250 | UpgradeLog*.htm
251 | ServiceFabricBackup/
252 | *.rptproj.bak
253 |
254 | # SQL Server files
255 | *.mdf
256 | *.ldf
257 | *.ndf
258 |
259 | # Business Intelligence projects
260 | *.rdl.data
261 | *.bim.layout
262 | *.bim_*.settings
263 | *.rptproj.rsuser
264 | *- [Bb]ackup.rdl
265 | *- [Bb]ackup ([0-9]).rdl
266 | *- [Bb]ackup ([0-9][0-9]).rdl
267 |
268 | # Microsoft Fakes
269 | FakesAssemblies/
270 |
271 | # GhostDoc plugin setting file
272 | *.GhostDoc.xml
273 |
274 | # Node.js Tools for Visual Studio
275 | .ntvs_analysis.dat
276 | node_modules/
277 |
278 | # Visual Studio 6 build log
279 | *.plg
280 |
281 | # Visual Studio 6 workspace options file
282 | *.opt
283 |
284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
285 | *.vbw
286 |
287 | # Visual Studio LightSwitch build output
288 | **/*.HTMLClient/GeneratedArtifacts
289 | **/*.DesktopClient/GeneratedArtifacts
290 | **/*.DesktopClient/ModelManifest.xml
291 | **/*.Server/GeneratedArtifacts
292 | **/*.Server/ModelManifest.xml
293 | _Pvt_Extensions
294 |
295 | # Paket dependency manager
296 | .paket/paket.exe
297 | paket-files/
298 |
299 | # FAKE - F# Make
300 | .fake/
301 |
302 | # CodeRush personal settings
303 | .cr/personal
304 |
305 | # Python Tools for Visual Studio (PTVS)
306 | __pycache__/
307 | *.pyc
308 |
309 | # Cake - Uncomment if you are using it
310 | # tools/**
311 | # !tools/packages.config
312 |
313 | # Tabs Studio
314 | *.tss
315 |
316 | # Telerik's JustMock configuration file
317 | *.jmconfig
318 |
319 | # BizTalk build output
320 | *.btp.cs
321 | *.btm.cs
322 | *.odx.cs
323 | *.xsd.cs
324 |
325 | # OpenCover UI analysis results
326 | OpenCover/
327 |
328 | # Azure Stream Analytics local run output
329 | ASALocalRun/
330 |
331 | # MSBuild Binary and Structured Log
332 | *.binlog
333 |
334 | # NVidia Nsight GPU debugger configuration file
335 | *.nvuser
336 |
337 | # MFractors (Xamarin productivity tool) working folder
338 | .mfractor/
339 |
340 | # Local History for Visual Studio
341 | .localhistory/
342 |
343 | # BeatPulse healthcheck temp database
344 | healthchecksdb
345 |
346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
347 | MigrationBackup/
348 |
349 | # Ionide (cross platform F# VS Code tools) working folder
350 | .ionide/
351 |
--------------------------------------------------------------------------------
/AspNetSaml.Tests/AspNetSaml.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/AspNetSaml.Tests/UnitTests.cs:
--------------------------------------------------------------------------------
1 | using Saml;
2 | using System.IO.Compression;
3 | using System.IO;
4 | using System.Text;
5 |
6 | namespace AspNetSaml.Tests
7 | {
8 | [TestClass]
9 | public class UnitTests
10 | {
11 | //cert and signature taken form here: www.samltool.com/generic_sso_res.php
12 |
13 | [TestMethod]
14 | public void TestSamlResponseValidator()
15 | {
16 | var cert = @"-----BEGIN CERTIFICATE-----
17 | MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==
18 | -----END CERTIFICATE-----";
19 |
20 | var samlresp = new Saml.Response(cert);
21 | samlresp.LoadXml(@"
22 |
23 | http://idp.example.com/metadata.php
24 |
25 |
26 | 99Bke1BpL1yOfGd5ADkGSle2sZg=OOyb3YtYQm3DC7gj6lQPM20r76HH4KvAE93f5xrIuIHGk8ZJlse4m8t4msLkhwUEAGwWOOVyHs8gChtN1m/P4pKCXyttO9Hev14Wz8E1R444kg5Yak+02FZ+Fn3VbbPq+kY4eYRkczNMphivWkdwc/QjDguNzGoKCEEtbBKDMGg=
27 | MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==
28 |
29 |
30 |
31 |
32 | http://idp.example.com/metadata.php
33 |
34 | _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7
35 |
36 |
37 |
38 |
39 |
40 |
41 | http://sp.example.com/demo1/metadata.php
42 |
43 |
44 |
45 |
46 | urn:oasis:names:tc:SAML:2.0:ac:classes:Password
47 |
48 |
49 |
50 |
51 | test
52 |
53 |
54 | test@example.com
55 |
56 |
57 | users
58 | examplerole1
59 |
60 |
61 |
62 |
63 | ");
64 |
65 | samlresp.CurrentTime = new DateTime(2022, 3, 25);
66 | Assert.IsTrue(samlresp.IsValid());
67 |
68 | Assert.IsTrue(samlresp.GetNameID() == "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7");
69 |
70 | Assert.IsTrue(samlresp.GetEmail() == "test@example.com");
71 |
72 | Assert.IsTrue(samlresp.GetCustomAttribute("uid") == "test");
73 | }
74 |
75 | [TestMethod]
76 | public void TestSamlSignoutResponseValidator()
77 | {
78 | //this test's cert and signature borrowed from https://github.com/boxyhq/jackson/
79 |
80 | var cert = @"-----BEGIN CERTIFICATE-----
81 | MIIDBzCCAe+gAwIBAgIJcp0xLOhRU0fTMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMTFmRldi10eWo3cXl6ei5hdXRoMC5jb20wHhcNMTkwMzI3MTMyMTQ0WhcNMzIxMjAzMTMyMTQ0WjAhMR8wHQYDVQQDExZkZXYtdHlqN3F5enouYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyr2LHhkTEf5xO+mGjZascQ9bfzcSDmjyJ6RxfD9rAJorqVDIcq+dEtxDvo0HWt/bccX+9AZmMiqCclLRyv7Sley7BkxYra5ym8mTwmaZqUZbWyCQ15Hpq6G27yrWk8V6WKvMhJoxDqlgFh08QDOxBy5jCzwxVyFKDchJiy1TflLC8dFJLcmszQsrvl3enbQyYy9XejgniugJKElZMZknFF9LmcQWeCmwDG+2w6HcMZIXPny9Cl5GZra7wt/EWg3iwNw5ZqP41Hulf9fhilJs3bVehnDgftQTKyTUBEfCDxzaIsEmpPWAqTg5IIEKkHX4/1Rm+7ltxg+n0pIXxUrtCQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRcb2UMMqwD9zCk3DOWnx/XwfKd5DAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAFE1FG/u0eYHk/R5a8gGiPgazEjmQUSMlBxjhhTU8bc0X/oLyCfJGdoXQKJVtHgKAIcvCtrHBjKDy8CwSn+J1jTMZklnpkhvXUHiEj1ViplupwuXblvhEXR2+Bkly57Uy1qoFvKHCejayRWsDaG062kEQkt5k1FtVatUGS6labThHjr8K2RyqTAYpXWqthR+wKTFLni9V2pjuoUOABBYeGTalnIOGvr/i5I+IjJDHND0x7wrveekFDI5yX9V8ZdMGiN2SkoXBMa5+o1aD3gtbi8c2HcOgjMsIzHGAj4dz/0syWfpkEkrbs7FURSvtuRLaNrH/2/rto0KgiWWuPKvm1w=
82 | -----END CERTIFICATE-----";
83 |
84 | var samlresp = new Saml.SignoutResponse(cert);
85 | samlresp.LoadXml(@"urn:dev-tyj7qyzz.auth0.comLk9TO/DGFFLLb+29H32O/scFccU=altTmKkKqudi+jYBZd6bETdYRbTKerUiNxFugcoD7ZmdZsRlrcNir0ZLRq+NB6nTh4zeKwGiGs03FyAW0Wdr8vgl0GQ/KOGuUrpoFNI8EID1HYrghHZMR43CgauIHGg0dw8uSjQYUcU1ICVYG2trgXC9TR81g+3XVBPBnoJWS2yV8hPc6QdFAUdb/0qUn/GPdpSPOlb6/MMUQB+K+es6HzjQfU2PEV3aNarHrKHSyFRdBHFMgtt7rUE3eAev+3/Uwq6RPBFk9huUJ6F0MRDoVjpWNzD2jByTtRv7OYInDsEJKCwJ+6pOKGVK6GDXuXnuI8s6BNEalpNJkWR8BxFVbw==MIIDBzCCAe+gAwIBAgIJcp0xLOhRU0fTMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMTFmRldi10eWo3cXl6ei5hdXRoMC5jb20wHhcNMTkwMzI3MTMyMTQ0WhcNMzIxMjAzMTMyMTQ0WjAhMR8wHQYDVQQDExZkZXYtdHlqN3F5enouYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyr2LHhkTEf5xO+mGjZascQ9bfzcSDmjyJ6RxfD9rAJorqVDIcq+dEtxDvo0HWt/bccX+9AZmMiqCclLRyv7Sley7BkxYra5ym8mTwmaZqUZbWyCQ15Hpq6G27yrWk8V6WKvMhJoxDqlgFh08QDOxBy5jCzwxVyFKDchJiy1TflLC8dFJLcmszQsrvl3enbQyYy9XejgniugJKElZMZknFF9LmcQWeCmwDG+2w6HcMZIXPny9Cl5GZra7wt/EWg3iwNw5ZqP41Hulf9fhilJs3bVehnDgftQTKyTUBEfCDxzaIsEmpPWAqTg5IIEKkHX4/1Rm+7ltxg+n0pIXxUrtCQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRcb2UMMqwD9zCk3DOWnx/XwfKd5DAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAFE1FG/u0eYHk/R5a8gGiPgazEjmQUSMlBxjhhTU8bc0X/oLyCfJGdoXQKJVtHgKAIcvCtrHBjKDy8CwSn+J1jTMZklnpkhvXUHiEj1ViplupwuXblvhEXR2+Bkly57Uy1qoFvKHCejayRWsDaG062kEQkt5k1FtVatUGS6labThHjr8K2RyqTAYpXWqthR+wKTFLni9V2pjuoUOABBYeGTalnIOGvr/i5I+IjJDHND0x7wrveekFDI5yX9V8ZdMGiN2SkoXBMa5+o1aD3gtbi8c2HcOgjMsIzHGAj4dz/0syWfpkEkrbs7FURSvtuRLaNrH/2/rto0KgiWWuPKvm1w=");
86 | Assert.IsTrue(samlresp.IsValid());
87 |
88 | Assert.IsTrue(samlresp.GetLogoutStatus() == "Success");
89 | }
90 |
91 | [TestMethod]
92 | public void TestSamlResponseValidatorAdvanced()
93 | {
94 | var cert = @"-----BEGIN CERTIFICATE-----
95 | MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=
96 | -----END CERTIFICATE-----";
97 |
98 | var samlresp = new Saml.Response(cert);
99 | samlresp.CurrentTime = new DateTime(2022, 3, 25);
100 | samlresp.LoadXml(@"http://keycloak:1080/realms/POCUrJzr9Ja0f4Ks+K6TPEfQ53bw1veGXHtMZpLmRrr/ww=EAM65nY/e0YkK/H0nw+hdt6PhUIEs5jtftvP/NuHCSFjsVNj8L4jIT7Gvso8r9gSnwz0FJetVK16LjHdN+0f8Od2BDk9njD7KBQx9v9ich12zl1Ny+T6dLtc4XypkvoPwscna7KIQOEn8xeKBq4IbC+gPYfJEQ3GjnQ5JuXhJW5GValLELKWbH21oECRL6VAs7BAohQy2/BbTTGM1tbeuqWIZrqdP/KKOpiHxVIPwzwC8EuQmrhYiaJ9tOzNtBJGD5IW7L6Z6GIhVX2yQPuEW/gfb/bYCi6+0KD664YBICfyJLSarbcK6qgafP9YUdJ48qopiHXbuZ1m8ceCfC0Kow==MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=http://keycloak:1080/realms/POCguestWebApp3urn:oasis:names:tc:SAML:2.0:ac:classes:unspecifiedguest@guest.comGuestGuestuma_authorizationoffline_accessdefault-roles-pocview-profilemanage-accountmanage-account-linksSimpleUser");
101 |
102 | Assert.IsTrue(samlresp.IsValid());
103 |
104 | Assert.IsTrue(samlresp.GetCustomAttributeViaFriendlyName("givenName") == "Guest");
105 |
106 | Assert.IsTrue(Enumerable.SequenceEqual(samlresp.GetCustomAttributeAsList("Role"), new List { "uma_authorization", "offline_access", "default-roles-poc", "view-profile", "manage-account", "manage-account-links", "SimpleUser" }));
107 | }
108 |
109 | [TestMethod]
110 | public void TestInvalidCertString()
111 | {
112 | //cert without the "-----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----"
113 | var cert = @"MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=
114 | -----END CERTIFICATE-----";
115 | var samlresp = new Saml.Response(cert) { CurrentTime = new DateTime(2022, 3, 25) };
116 | samlresp.LoadXml(@"http://keycloak:1080/realms/POCUrJzr9Ja0f4Ks+K6TPEfQ53bw1veGXHtMZpLmRrr/ww=EAM65nY/e0YkK/H0nw+hdt6PhUIEs5jtftvP/NuHCSFjsVNj8L4jIT7Gvso8r9gSnwz0FJetVK16LjHdN+0f8Od2BDk9njD7KBQx9v9ich12zl1Ny+T6dLtc4XypkvoPwscna7KIQOEn8xeKBq4IbC+gPYfJEQ3GjnQ5JuXhJW5GValLELKWbH21oECRL6VAs7BAohQy2/BbTTGM1tbeuqWIZrqdP/KKOpiHxVIPwzwC8EuQmrhYiaJ9tOzNtBJGD5IW7L6Z6GIhVX2yQPuEW/gfb/bYCi6+0KD664YBICfyJLSarbcK6qgafP9YUdJ48qopiHXbuZ1m8ceCfC0Kow==MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=http://keycloak:1080/realms/POCguestWebApp3urn:oasis:names:tc:SAML:2.0:ac:classes:unspecifiedguest@guest.comGuestGuestuma_authorizationoffline_accessdefault-roles-pocview-profilemanage-accountmanage-account-linksSimpleUser");
117 | Assert.IsTrue(samlresp.IsValid());
118 | Assert.IsTrue(samlresp.GetCustomAttributeViaFriendlyName("givenName") == "Guest");
119 |
120 | //without "-----END CERTIFICATE-----"
121 | cert = @"-----BEGIN CERTIFICATE-----
122 | MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=";
123 | samlresp = new Saml.Response(cert) { CurrentTime = new DateTime(2022, 3, 25) };
124 | samlresp.LoadXml(@"http://keycloak:1080/realms/POCUrJzr9Ja0f4Ks+K6TPEfQ53bw1veGXHtMZpLmRrr/ww=EAM65nY/e0YkK/H0nw+hdt6PhUIEs5jtftvP/NuHCSFjsVNj8L4jIT7Gvso8r9gSnwz0FJetVK16LjHdN+0f8Od2BDk9njD7KBQx9v9ich12zl1Ny+T6dLtc4XypkvoPwscna7KIQOEn8xeKBq4IbC+gPYfJEQ3GjnQ5JuXhJW5GValLELKWbH21oECRL6VAs7BAohQy2/BbTTGM1tbeuqWIZrqdP/KKOpiHxVIPwzwC8EuQmrhYiaJ9tOzNtBJGD5IW7L6Z6GIhVX2yQPuEW/gfb/bYCi6+0KD664YBICfyJLSarbcK6qgafP9YUdJ48qopiHXbuZ1m8ceCfC0Kow==MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=http://keycloak:1080/realms/POCguestWebApp3urn:oasis:names:tc:SAML:2.0:ac:classes:unspecifiedguest@guest.comGuestGuestuma_authorizationoffline_accessdefault-roles-pocview-profilemanage-accountmanage-account-linksSimpleUser");
125 | Assert.IsTrue(samlresp.IsValid());
126 | Assert.IsTrue(samlresp.GetCustomAttributeViaFriendlyName("givenName") == "Guest");
127 |
128 | //without "-----BEGIN CERTIFICATE-----"
129 | cert = @"MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=
130 | -----END CERTIFICATE-----";
131 | samlresp = new Saml.Response(cert) { CurrentTime = new DateTime(2022, 3, 25) };
132 | samlresp.LoadXml(@"http://keycloak:1080/realms/POCUrJzr9Ja0f4Ks+K6TPEfQ53bw1veGXHtMZpLmRrr/ww=EAM65nY/e0YkK/H0nw+hdt6PhUIEs5jtftvP/NuHCSFjsVNj8L4jIT7Gvso8r9gSnwz0FJetVK16LjHdN+0f8Od2BDk9njD7KBQx9v9ich12zl1Ny+T6dLtc4XypkvoPwscna7KIQOEn8xeKBq4IbC+gPYfJEQ3GjnQ5JuXhJW5GValLELKWbH21oECRL6VAs7BAohQy2/BbTTGM1tbeuqWIZrqdP/KKOpiHxVIPwzwC8EuQmrhYiaJ9tOzNtBJGD5IW7L6Z6GIhVX2yQPuEW/gfb/bYCi6+0KD664YBICfyJLSarbcK6qgafP9YUdJ48qopiHXbuZ1m8ceCfC0Kow==MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=http://keycloak:1080/realms/POCguestWebApp3urn:oasis:names:tc:SAML:2.0:ac:classes:unspecifiedguest@guest.comGuestGuestuma_authorizationoffline_accessdefault-roles-pocview-profilemanage-accountmanage-account-linksSimpleUser");
133 | Assert.IsTrue(samlresp.IsValid());
134 | Assert.IsTrue(samlresp.GetCustomAttributeViaFriendlyName("givenName") == "Guest");
135 |
136 | }
137 |
138 | [TestMethod]
139 | public void TestSamlRequest()
140 | {
141 |
142 | var request = new AuthRequest(
143 | "http://www.myapp.com",
144 | "http://www.myapp.com/SamlConsume"
145 | );
146 |
147 | var r = request.GetRequest();
148 |
149 | //decode the compressed base64
150 | var ms = new MemoryStream(Convert.FromBase64String(r));
151 | var ds = new DeflateStream(ms, CompressionMode.Decompress, true);
152 | var output = new MemoryStream();
153 | ds.CopyTo(output);
154 |
155 | //get xml
156 | var str = Encoding.UTF8.GetString(output.ToArray());
157 |
158 | Assert.IsTrue(str.EndsWith(@"ProtocolBinding=""urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"" AssertionConsumerServiceURL=""http://www.myapp.com/SamlConsume"" xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol"">http://www.myapp.com"));
159 |
160 | }
161 |
162 | [TestMethod]
163 | public void TestStringToByteArray()
164 | {
165 | //test that the old StringToByteArray was generating same result as the new Encoding.ASCII.GetBytes
166 |
167 | var cert = @"-----BEGIN CERTIFICATE-----
168 | MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==
169 | -----END CERTIFICATE-----";
170 |
171 |
172 | var x = StringToByteArray(cert);
173 | var y = Encoding.ASCII.GetBytes(cert);
174 | Assert.IsTrue(x.SequenceEqual(y));
175 | }
176 |
177 | private static byte[] StringToByteArray(string st)
178 | {
179 | byte[] bytes = new byte[st.Length];
180 | for (int i = 0; i < st.Length; i++)
181 | {
182 | bytes[i] = (byte)st[i];
183 | }
184 | return bytes;
185 | }
186 |
187 | [TestMethod]
188 | public void TestIdpLogoutRequest()
189 | {
190 | // test data borrowed from https://github.com/wso2-extensions/identity-inbound-auth-saml/blob/master/components/org.wso2.carbon.identity.sso.saml/src/test/java/org/wso2/carbon/identity/sso/saml/TestConstants.java
191 |
192 | var cert = @"-----BEGIN CERTIFICATE-----
193 | MIIDSTCCAjGgAwIBAgIEAoLQ/TANBgkqhkiG9w0BAQsFADBVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxDTALBgNVBAoTBFdTTzIxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNzA3MTkwNjUyNTFaFw0yNzA3MTcwNjUyNTFaMFUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzENMAsGA1UEChMEV1NPMjESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAluZFdW1ynitztkWLC6xKegbRWxky+5P0p4ShYEOkHs30QI2VCuR6Qo4Bz5rTgLBrky03W1GAVrZxuvKRGj9V9+PmjdGtau4CTXu9pLLcqnruaczoSdvBYA3lS9a7zgFU0+s6kMl2EhB+rk7gXluEep7lIOenzfl2f6IoTKa2fVgVd3YKiSGsyL4tztS70vmmX121qm0sTJdKWP4HxXyqK9neolXI9fYyHOYILVNZ69z/73OOVhkh/mvTmWZLM7GM6sApmyLX6OXUp8z0pkY+vT/9+zRxxQs7GurC4/C1nK3rI/0ySUgGEafO1atNjYmlFN+M3tZX6nEcA6g94IavyQIDAQABoyEwHzAdBgNVHQ4EFgQUtS8kIYxQ8UVvVrZSdgyide9OHxUwDQYJKoZIhvcNAQELBQADggEBABfk5mqsVUrpFCYTZZhOxTRRpGXqoW1G05bOxHxs42Paxw8rAJ06Pty9jqM1CgRPpqvZa2lPQBQqZrHkdDE06q4NG0DqMH8NT+tNkXBe9YTre3EJCSfsvswtLVDZ7GDvTHKojJjQvdVCzRj6XH5Truwefb4BJz9APtnlyJIvjHk1hdozqyOniVZd0QOxLAbcdt946chNdQvCm6aUOputp8Xogr0KBnEy3U8es2cAfNZaEkPU8Va5bU6Xjny8zGQnXCXxPKp7sMpgO93nPBt/liX1qfyXM7xEotWoxmm6HZx8oWQ8U5aiXjZ5RKDWCCq4ZuXl6wVsUz1iE61suO5yWi8=
194 | -----END CERTIFICATE-----";
195 |
196 | var logoutRequestXml = "\n" +
197 | "travelocity.com" +
202 | "" +
203 | "" +
204 | "3573j3HyxkMId6VUCbBuakX259Q=" +
209 | "Y6YDnJFEFa8kG43m7aItV8dBiRlK" +
210 | "/xgngt7T0X3e+11C1KXDvD+w2kOnLu4M8ZO7Mn7nWMx0Qyw32CBl+4Z86wK8b6aGQYbJ6qm26JVBdRKAhYaCxlLbVRMRDWG" +
211 | "nD1dtczR5SR8XZBXyUAa0utmzjBXfIvi0vDzNoBqPgM0tURJ5jMQtMVdE1wpXO89L7Ub4M/CKy2ooDrSCmW0sgQCfi9Gy1p" +
212 | "eNfkEn+tcfXjz9hAb1wIKg9aoVvrthnSk9UPLiLAgM75eQL6Vijua5ZXagmza4j5H0GujEN8yCZrEzgW4hZNx4OViD1euYj" +
213 | "xNSQ4tqkeoeID5tFt9DxrUZsVtiWw==" +
214 | "MIIDSTCCAjGgAwIBAgIEAoLQ/TANBgkqhkiG9w0BAQsFADBVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAc" +
215 | "TDU1vdW50YWluIFZpZXcxDTALBgNVBAoTBFdTTzIxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNzA3MTkwNjUyNTFaFw0yNz" +
216 | "A3MTcwNjUyNTFaMFUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzENMAsGA1UEC" +
217 | "hMEV1NPMjESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAluZFdW1ynitztkWL" +
218 | "C6xKegbRWxky+5P0p4ShYEOkHs30QI2VCuR6Qo4Bz5rTgLBrky03W1GAVrZxuvKRGj9V9+PmjdGtau4CTXu9pLLcqnruacz" +
219 | "oSdvBYA3lS9a7zgFU0+s6kMl2EhB+rk7gXluEep7lIOenzfl2f6IoTKa2fVgVd3YKiSGsyL4tztS70vmmX121qm0sTJdKWP" +
220 | "4HxXyqK9neolXI9fYyHOYILVNZ69z/73OOVhkh/mvTmWZLM7GM6sApmyLX6OXUp8z0pkY+vT/9+zRxxQs7GurC4/C1nK3rI" +
221 | "/0ySUgGEafO1atNjYmlFN+M3tZX6nEcA6g94IavyQIDAQABoyEwHzAdBgNVHQ4EFgQUtS8kIYxQ8UVvVrZSdgyide9OHxUw" +
222 | "DQYJKoZIhvcNAQELBQADggEBABfk5mqsVUrpFCYTZZhOxTRRpGXqoW1G05bOxHxs42Paxw8rAJ06Pty9jqM1CgRPpqvZa2l" +
223 | "PQBQqZrHkdDE06q4NG0DqMH8NT+tNkXBe9YTre3EJCSfsvswtLVDZ7GDvTHKojJjQvdVCzRj6XH5Truwefb4BJz9APtnlyJ" +
224 | "IvjHk1hdozqyOniVZd0QOxLAbcdt946chNdQvCm6aUOputp8Xogr0KBnEy3U8es2cAfNZaEkPU8Va5bU6Xjny8zGQnXCXxP" +
225 | "Kp7sMpgO93nPBt/liX1qfyXM7xEotWoxmm6HZx8oWQ8U5aiXjZ5RKDWCCq4ZuXl6wVsUz1iE61suO5yWi8=egon@got.com@is.com" +
229 | "8c8354d5-4eea-4942-8651-ce12fa4626ea";
230 |
231 | var request = new Saml.IdpLogoutRequest(cert);
232 | request.LoadXml(logoutRequestXml);
233 |
234 | Assert.IsTrue(request.IsValid());
235 | Assert.AreEqual("egon@got.com@is.com", request.GetNameID());
236 | Assert.AreEqual("8c8354d5-4eea-4942-8651-ce12fa4626ea", request.GetSessionIndex());
237 | }
238 | }
239 | }
--------------------------------------------------------------------------------
/AspNetSaml.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using Microsoft.VisualStudio.TestTools.UnitTesting;
--------------------------------------------------------------------------------
/AspNetSaml.WebExample/AspNetSaml.WebExample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/AspNetSaml.WebExample/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | // TODO: specify the certificate that your SAML provider gave you
4 | // your app's entity ID
5 | // and the SAML provider's endpoint (where we should redirect the user)
6 | const string SAML_CERTIFICATE = """
7 | -----BEGIN CERTIFICATE-----
8 | BLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAH123543==
9 | -----END CERTIFICATE-----
10 | """;
11 | const string ENTITY_ID = "[YOUR_ENTITY_ID]";
12 | const string SAML_ENDPOINT = "http://saml-provider-that-we-use.com/login/";
13 |
14 | var builder = WebApplication.CreateBuilder(args);
15 | var app = builder.Build();
16 | app.UseHttpsRedirection();
17 |
18 |
19 | //homepage
20 | app.MapGet("/", () =>
21 | {
22 | var request = new Saml.AuthRequest(
23 | ENTITY_ID,
24 | "https://localhost:7009/SamlConsume"
25 | );
26 |
27 | //now send the user to the SAML provider
28 | var url = request.GetRedirectUrl(SAML_ENDPOINT);
29 |
30 | return Results.Content("Click here to log in", "text/html");
31 | });
32 |
33 |
34 | //IsP will send logged in user here
35 | app.MapPost("/SamlConsume", ([FromForm] string samlResponse) =>
36 | {
37 | var saml = new Saml.Response(SAML_CERTIFICATE, samlResponse);
38 |
39 | if (saml.IsValid()) //all good?
40 | {
41 | return Results.Content("Success! Logged in as user " + saml.GetNameID(), "text/html");
42 | }
43 |
44 | return Results.Unauthorized();
45 | });
46 |
47 |
48 | //IdP will send logout requests here
49 | app.MapPost("/SamlLogout", ([FromForm] string samlResponse) =>
50 | {
51 | var saml = new Saml.IdpLogoutRequest(SAML_CERTIFICATE, samlResponse);
52 |
53 | if (saml.IsValid()) //all good?
54 | {
55 | var username = saml.GetNameID();
56 | //pseudo-code-logout-user-from-your-system(username);
57 | }
58 |
59 | return Results.Ok();
60 | });
61 |
62 | app.Run();
--------------------------------------------------------------------------------
/AspNetSaml.WebExample/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "https": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": true,
8 | "applicationUrl": "https://localhost:7009;http://localhost:5266",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/AspNetSaml.WebExample/README:
--------------------------------------------------------------------------------
1 | This is a simple example of how to use the AspNetSaml library.
2 |
3 | A very basic "minimal API" style app ASP.NET 8
--------------------------------------------------------------------------------
/AspNetSaml.WebExample/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/AspNetSaml.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.4.33213.308
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetSaml", "AspNetSaml\AspNetSaml.csproj", "{E52EF883-4403-4CC6-A7B4-4676A83E8E94}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetSaml.Tests", "AspNetSaml.Tests\AspNetSaml.Tests.csproj", "{6633016C-09D8-4B86-876A-6A6E995D5734}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetSaml.WebExample", "AspNetSaml.WebExample\AspNetSaml.WebExample.csproj", "{96C75360-DCD9-4EF3-80A4-BE91032B32DE}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {E52EF883-4403-4CC6-A7B4-4676A83E8E94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {E52EF883-4403-4CC6-A7B4-4676A83E8E94}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {E52EF883-4403-4CC6-A7B4-4676A83E8E94}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {E52EF883-4403-4CC6-A7B4-4676A83E8E94}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {6633016C-09D8-4B86-876A-6A6E995D5734}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {6633016C-09D8-4B86-876A-6A6E995D5734}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {6633016C-09D8-4B86-876A-6A6E995D5734}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {6633016C-09D8-4B86-876A-6A6E995D5734}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {96C75360-DCD9-4EF3-80A4-BE91032B32DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {96C75360-DCD9-4EF3-80A4-BE91032B32DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {96C75360-DCD9-4EF3-80A4-BE91032B32DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {96C75360-DCD9-4EF3-80A4-BE91032B32DE}.Release|Any CPU.Build.0 = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(SolutionProperties) = preSolution
32 | HideSolutionNode = FALSE
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {C07DD55F-FC63-4715-AC41-E6879797FABA}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/AspNetSaml/AspNetSaml.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | AspNetSaml
6 | AspNetSaml
7 | 2.1.3
8 | Alex from Jitbit
9 | Copyright Jitbit 2017-2023
10 | https://github.com/jitbit/AspNetSaml
11 | README.md
12 | https://github.com/jitbit/AspNetSaml
13 | SAML component for ASP.NET
14 | saml;sso;asp.net
15 |
16 |
17 |
18 |
19 | True
20 | \
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/AspNetSaml/Saml.cs:
--------------------------------------------------------------------------------
1 | /* Jitbit's simple SAML 2.0 component for ASP.NET
2 | https://github.com/jitbit/AspNetSaml/
3 | (c) Jitbit LP, 2016-2025
4 | Use this freely under the Apache license (see https://choosealicense.com/licenses/apache-2.0/)
5 | */
6 |
7 | using System;
8 | using System.Collections.Generic;
9 | using System.IO;
10 | using System.Linq;
11 | using System.Xml;
12 | using System.Security.Cryptography.X509Certificates;
13 | using System.Security.Cryptography.Xml;
14 | using System.IO.Compression;
15 | using System.Text;
16 | using System.Runtime;
17 |
18 | namespace Saml
19 | {
20 | public abstract class BaseSamlMessage
21 | {
22 | protected XmlDocument _xmlDoc;
23 | protected readonly X509Certificate2 _certificate;
24 | protected XmlNamespaceManager _xmlNameSpaceManager; //we need this one to run our XPath queries on the SAML XML
25 |
26 | public string Xml { get { return _xmlDoc.OuterXml; } }
27 |
28 | public BaseSamlMessage(string certificateStr, string responseString = null) : this(Encoding.ASCII.GetBytes(EnsureCertFormat(certificateStr)), responseString) { }
29 |
30 | public BaseSamlMessage(byte[] certificateBytes, string responseString = null)
31 | {
32 | _certificate = new X509Certificate2(certificateBytes);
33 | if (responseString != null)
34 | LoadXmlFromBase64(responseString);
35 | }
36 |
37 | ///
38 | /// Parse SAML response XML (in case was it not passed in constructor)
39 | ///
40 | public void LoadXml(string xml)
41 | {
42 | _xmlDoc = new XmlDocument { PreserveWhitespace = true, XmlResolver = null };
43 | _xmlDoc.LoadXml(xml);
44 |
45 | _xmlNameSpaceManager = GetNamespaceManager(); //lets construct a "manager" for XPath queries
46 | }
47 |
48 | //linux fix (not working when there's no "-----BEGIN CERTIFICATE-----xxxx-----END CERTIFICATE-----"
49 | //also remove double line breaks
50 | private static string EnsureCertFormat(string cert)
51 | {
52 | var samlCertificate = cert.Replace("\r", "").Replace("\n\n", "\n");
53 | if (!samlCertificate.StartsWith("-----BEGIN CERTIFICATE-----"))
54 | {
55 | samlCertificate = "-----BEGIN CERTIFICATE-----\n" + samlCertificate.Trim();
56 | }
57 | if (!samlCertificate.EndsWith("-----END CERTIFICATE-----"))
58 | {
59 | samlCertificate = samlCertificate.Trim() + "\n-----END CERTIFICATE-----";
60 | }
61 |
62 | return samlCertificate;
63 | }
64 |
65 | public void LoadXmlFromBase64(string response)
66 | {
67 | UTF8Encoding enc = new UTF8Encoding();
68 | LoadXml(enc.GetString(Convert.FromBase64String(response)));
69 | }
70 |
71 | //an XML signature can "cover" not the whole document, but only a part of it
72 | //.NET's built in "CheckSignature" does not cover this case, it will validate to true.
73 | //We should check the signature reference, so it "references" the id of the root document element! If not - it's a hack
74 | protected bool ValidateSignatureReference(SignedXml signedXml)
75 | {
76 | if (signedXml.SignedInfo.References.Count != 1) //no ref at all
77 | return false;
78 |
79 | var reference = (Reference)signedXml.SignedInfo.References[0];
80 | var id = reference.Uri.Substring(1);
81 |
82 | var idElement = signedXml.GetIdElement(_xmlDoc, id);
83 |
84 | if (idElement == _xmlDoc.DocumentElement)
85 | return true;
86 | else //sometimes its not the "root" doc-element that is being signed, but the "assertion" element
87 | {
88 | var assertionNode = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion", _xmlNameSpaceManager) as XmlElement;
89 | if (assertionNode != idElement)
90 | return false;
91 | }
92 |
93 | return true;
94 | }
95 |
96 | //returns namespace manager, we need one b/c MS says so... Otherwise XPath doesnt work in an XML doc with namespaces
97 | //see https://stackoverflow.com/questions/7178111/why-is-xmlnamespacemanager-necessary
98 | private XmlNamespaceManager GetNamespaceManager()
99 | {
100 | XmlNamespaceManager manager = new XmlNamespaceManager(_xmlDoc.NameTable);
101 | manager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);
102 | manager.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion");
103 | manager.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol");
104 |
105 | return manager;
106 | }
107 |
108 | ///
109 | /// Checks the validity of SAML response (validate signature, check expiration date etc)
110 | ///
111 | ///
112 | public bool IsValid()
113 | {
114 | XmlNodeList nodeList = _xmlDoc.SelectNodes("//ds:Signature", _xmlNameSpaceManager);
115 |
116 | SignedXml signedXml = new SignedXml(_xmlDoc);
117 |
118 | if (nodeList.Count == 0) return false;
119 |
120 | signedXml.LoadXml((XmlElement)nodeList[0]);
121 | return ValidateSignatureReference(signedXml) && signedXml.CheckSignature(_certificate, true) && !IsExpired();
122 | }
123 |
124 | protected virtual bool IsExpired()
125 | {
126 | DateTime expirationDate = DateTime.MaxValue;
127 | XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData", _xmlNameSpaceManager);
128 | if (node != null && node.Attributes["NotOnOrAfter"] != null)
129 | {
130 | DateTime.TryParse(node.Attributes["NotOnOrAfter"].Value, out expirationDate);
131 | }
132 | return (CurrentTime ?? DateTime.UtcNow) > expirationDate.ToUniversalTime();
133 | }
134 |
135 | public DateTime? CurrentTime { get; set; } = null; //mostly for unit-testing. STUPID I KNOW, will fix later
136 | }
137 |
138 | public class Response : BaseSamlMessage
139 | {
140 | public Response(string certificateStr, string responseString = null) : base(certificateStr, responseString) { }
141 |
142 | public Response(byte[] certificateBytes, string responseString = null) : base(certificateBytes, responseString) { }
143 |
144 | ///
145 | /// returns the User's login
146 | ///
147 | public string GetNameID()
148 | {
149 | XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:Subject/saml:NameID", _xmlNameSpaceManager);
150 | return node.InnerText;
151 | }
152 |
153 | public virtual string GetUpn()
154 | {
155 | return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn");
156 | }
157 |
158 | public virtual string GetEmail()
159 | {
160 | return GetCustomAttribute("User.email")
161 | ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress") //some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
162 | ?? GetCustomAttribute("mail"); //some providers put last name into an attribute named "mail"
163 | }
164 |
165 | public virtual string GetFirstName()
166 | {
167 | return GetCustomAttribute("first_name")
168 | ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname") //some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
169 | ?? GetCustomAttribute("User.FirstName")
170 | ?? GetCustomAttribute("givenName"); //some providers put last name into an attribute named "givenName"
171 | }
172 |
173 | public virtual string GetLastName()
174 | {
175 | return GetCustomAttribute("last_name")
176 | ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname") //some providers (for example Azure AD) put last name into an attribute named "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
177 | ?? GetCustomAttribute("User.LastName")
178 | ?? GetCustomAttribute("sn"); //some providers put last name into an attribute named "sn"
179 | }
180 |
181 | public virtual string GetDepartment()
182 | {
183 | return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department")
184 | ?? GetCustomAttribute("department");
185 | }
186 |
187 | public virtual string GetPhone()
188 | {
189 | return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone")
190 | ?? GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/telephonenumber");
191 | }
192 |
193 | public virtual string GetCompany()
194 | {
195 | return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/companyname")
196 | ?? GetCustomAttribute("organization")
197 | ?? GetCustomAttribute("User.CompanyName");
198 | }
199 |
200 | public virtual string GetLocation()
201 | {
202 | return GetCustomAttribute("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/location")
203 | ?? GetCustomAttribute("physicalDeliveryOfficeName");
204 | }
205 |
206 | public string GetCustomAttribute(string attr)
207 | {
208 | XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
209 | return node?.InnerText;
210 | }
211 |
212 | public string GetCustomAttributeViaFriendlyName(string attr)
213 | {
214 | XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@FriendlyName='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
215 | return node?.InnerText;
216 | }
217 |
218 | public List GetCustomAttributeAsList(string attr)
219 | {
220 | XmlNodeList nodes = _xmlDoc.SelectNodes("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
221 | return nodes?.Cast().Select(x => x.InnerText).ToList();
222 | }
223 | }
224 |
225 | ///
226 | /// Represents IdP-generated Logout Response in response to a SP-initiated Logout Request.
227 | ///
228 | public class SignoutResponse : BaseSamlMessage
229 | {
230 | public SignoutResponse(string certificateStr, string responseString = null) : base(certificateStr, responseString) { }
231 |
232 | public SignoutResponse(byte[] certificateBytes, string responseString = null) : base(certificateBytes, responseString) { }
233 |
234 | public string GetLogoutStatus()
235 | {
236 | XmlNode node = _xmlDoc.SelectSingleNode("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", _xmlNameSpaceManager);
237 | return node?.Attributes["Value"].Value.Replace("urn:oasis:names:tc:SAML:2.0:status:", string.Empty);
238 | }
239 | }
240 |
241 | ///
242 | /// Represents an IdP-initiated Logout Request received by the SP.
243 | ///
244 | public class IdpLogoutRequest : BaseSamlMessage
245 | {
246 | public IdpLogoutRequest(string certificateStr, string responseString = null) : base(certificateStr, responseString) { }
247 |
248 | public IdpLogoutRequest(byte[] certificateBytes, string responseString = null) : base(certificateBytes, responseString) { }
249 |
250 | ///
251 | /// Gets the NameID from the IdP-initiated LogoutRequest.
252 | ///
253 | public string GetNameID()
254 | {
255 | // LogoutRequest typically uses /samlp:LogoutRequest/saml:NameID
256 | XmlNode node = _xmlDoc.SelectSingleNode("/samlp:LogoutRequest/saml:NameID", _xmlNameSpaceManager);
257 | return node?.InnerText;
258 | }
259 |
260 | ///
261 | /// Gets the SessionIndex from the IdP-initiated LogoutRequest.
262 | ///
263 | /// The SessionIndex string, or null if not found.
264 | public string GetSessionIndex()
265 | {
266 | // SessionIndex is optional in the SAML spec for LogoutRequest
267 | XmlNode node = _xmlDoc.SelectSingleNode("/samlp:LogoutRequest/samlp:SessionIndex", _xmlNameSpaceManager);
268 | return node?.InnerText;
269 | }
270 |
271 | ///
272 | /// Checks the validity of the SAML IdP-initiated LogoutRequest (validate signature).
273 | /// This class relies on the base IsValid() method but overrides IsExpired() to always return false,
274 | /// effectively bypassing the expiration check which is not relevant for LogoutRequests.
275 | ///
276 | protected override bool IsExpired()
277 | {
278 | // LogoutRequests don't have the standard expiration elements.
279 | // Return false to ensure the base IsValid() check doesn't fail due to expiration.
280 | return false;
281 | }
282 | }
283 |
284 | public abstract class BaseRequest
285 | {
286 | public string _id;
287 | protected string _issue_instant;
288 |
289 | protected string _issuer;
290 |
291 | public BaseRequest(string issuer)
292 | {
293 | _id = "_" + Guid.NewGuid().ToString();
294 | _issue_instant = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);
295 |
296 | _issuer = issuer;
297 | }
298 |
299 | public abstract string GetRequest();
300 |
301 | protected static string ConvertToBase64Deflated(string input)
302 | {
303 | //byte[] toEncodeAsBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(input);
304 | //return System.Convert.ToBase64String(toEncodeAsBytes);
305 |
306 | //https://stackoverflow.com/questions/25120025/acs75005-the-request-is-not-a-valid-saml2-protocol-message-is-showing-always%3C/a%3E
307 | var memoryStream = new MemoryStream();
308 | using (var writer = new StreamWriter(new DeflateStream(memoryStream, CompressionMode.Compress, true), new UTF8Encoding(false)))
309 | {
310 | writer.Write(input);
311 | writer.Close();
312 | }
313 | string result = Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length, Base64FormattingOptions.None);
314 | return result;
315 | }
316 |
317 | ///
318 | /// returns the URL you should redirect your users to (i.e. your SAML-provider login URL with the Base64-ed request in the querystring
319 | ///
320 | /// SAML provider login url
321 | /// Optional state to pass through
322 | ///
323 | public string GetRedirectUrl(string samlEndpoint, string relayState = null)
324 | {
325 | var queryStringSeparator = samlEndpoint.Contains("?") ? "&" : "?";
326 |
327 | var url = samlEndpoint + queryStringSeparator + "SAMLRequest=" + Uri.EscapeDataString(GetRequest());
328 |
329 | if (!string.IsNullOrEmpty(relayState))
330 | {
331 | url += "&RelayState=" + Uri.EscapeDataString(relayState);
332 | }
333 |
334 | return url;
335 | }
336 | }
337 |
338 | public class AuthRequest : BaseRequest
339 | {
340 | private string _assertionConsumerServiceUrl;
341 |
342 | ///
343 | /// Initializes new instance of AuthRequest
344 | ///
345 | /// put your EntityID here
346 | /// put your return URL here
347 | public AuthRequest(string issuer, string assertionConsumerServiceUrl) : base(issuer)
348 | {
349 | _assertionConsumerServiceUrl = assertionConsumerServiceUrl;
350 | }
351 |
352 | ///
353 | /// get or sets if ForceAuthn attribute is sent to IdP
354 | ///
355 | public bool ForceAuthn { get; set; }
356 |
357 | [Obsolete("Obsolete, will be removed")]
358 | public enum AuthRequestFormat
359 | {
360 | Base64 = 1
361 | }
362 |
363 | [Obsolete("Obsolete, will be removed, use GetRequest()")]
364 | public string GetRequest(AuthRequestFormat format) => GetRequest();
365 |
366 | ///
367 | /// returns SAML request as compressed and Base64 encoded XML. You don't need this method
368 | ///
369 | ///
370 | public override string GetRequest()
371 | {
372 | using (StringWriter sw = new StringWriter())
373 | {
374 | XmlWriterSettings xws = new XmlWriterSettings { OmitXmlDeclaration = true };
375 |
376 | using (XmlWriter xw = XmlWriter.Create(sw, xws))
377 | {
378 | xw.WriteStartElement("samlp", "AuthnRequest", "urn:oasis:names:tc:SAML:2.0:protocol");
379 | xw.WriteAttributeString("ID", _id);
380 | xw.WriteAttributeString("Version", "2.0");
381 | xw.WriteAttributeString("IssueInstant", _issue_instant);
382 | xw.WriteAttributeString("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
383 | xw.WriteAttributeString("AssertionConsumerServiceURL", _assertionConsumerServiceUrl);
384 | if (ForceAuthn)
385 | xw.WriteAttributeString("ForceAuthn", "true");
386 |
387 | xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion");
388 | xw.WriteString(_issuer);
389 | xw.WriteEndElement();
390 |
391 | xw.WriteStartElement("samlp", "NameIDPolicy", "urn:oasis:names:tc:SAML:2.0:protocol");
392 | xw.WriteAttributeString("Format", "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified");
393 | xw.WriteAttributeString("AllowCreate", "true");
394 | xw.WriteEndElement();
395 |
396 | /*xw.WriteStartElement("samlp", "RequestedAuthnContext", "urn:oasis:names:tc:SAML:2.0:protocol");
397 | xw.WriteAttributeString("Comparison", "exact");
398 | xw.WriteStartElement("saml", "AuthnContextClassRef", "urn:oasis:names:tc:SAML:2.0:assertion");
399 | xw.WriteString("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport");
400 | xw.WriteEndElement();
401 | xw.WriteEndElement();*/
402 |
403 | xw.WriteEndElement();
404 | }
405 |
406 | return ConvertToBase64Deflated(sw.ToString());
407 | }
408 | }
409 | }
410 |
411 | ///
412 | /// Represents an SP-initiated Logout Request to be sent to the IdP.
413 | ///
414 | public class SignoutRequest : BaseRequest
415 | {
416 | private string _nameId;
417 |
418 | public SignoutRequest(string issuer, string nameId) : base(issuer)
419 | {
420 | _nameId = nameId;
421 | }
422 |
423 | public override string GetRequest()
424 | {
425 | using (StringWriter sw = new StringWriter())
426 | {
427 | XmlWriterSettings xws = new XmlWriterSettings { OmitXmlDeclaration = true };
428 |
429 | using (XmlWriter xw = XmlWriter.Create(sw, xws))
430 | {
431 | xw.WriteStartElement("samlp", "LogoutRequest", "urn:oasis:names:tc:SAML:2.0:protocol");
432 | xw.WriteAttributeString("ID", _id);
433 | xw.WriteAttributeString("Version", "2.0");
434 | xw.WriteAttributeString("IssueInstant", _issue_instant);
435 |
436 | xw.WriteStartElement("saml", "Issuer", "urn:oasis:names:tc:SAML:2.0:assertion");
437 | xw.WriteString(_issuer);
438 | xw.WriteEndElement();
439 |
440 | xw.WriteStartElement("saml", "NameID", "urn:oasis:names:tc:SAML:2.0:assertion");
441 | xw.WriteString(_nameId);
442 | xw.WriteEndElement();
443 |
444 | xw.WriteEndElement();
445 | }
446 |
447 | return ConvertToBase64Deflated(sw.ToString());
448 | }
449 | }
450 | }
451 |
452 | public static class MetaData
453 | {
454 | ///
455 | /// generates XML string describing service provider metadata based on provided EntiytID and Consumer URL
456 | ///
457 | /// Your SP EntityID
458 | /// Your Assertion Consumer Service URL (where IdP sends responses)
459 | /// Optional: Your Single Logout Service URL (where IdP sends LogoutRequests)
460 | /// XML metadata string
461 | public static string Generate(string entityId, string assertionConsumerServiceUrl, string singleLogoutServiceUrl = null)
462 | {
463 | string sloServiceElement = "";
464 | if (!string.IsNullOrEmpty(singleLogoutServiceUrl))
465 | {
466 | // We advertise HTTP-POST binding as IdpLogoutRequest handles POST
467 | sloServiceElement = $@"
468 | ";
469 | }
470 |
471 | // Construct the final metadata XML
472 | // NOTE: Using string interpolation with $@ can be tricky with complex XML and quotes.
473 | // Consider using XmlWriter or Linq to XML for more robust XML generation if needed.
474 | return $@"
475 |
478 |
479 |
480 |
481 | urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
482 |
483 | {sloServiceElement}
486 |
487 | ";
488 | }
489 | }
490 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2018 Jitbit LP
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # AspNetSaml
4 |
5 | Very short and simple SAML 2.0 "consumer" implementation in C#.
6 |
7 | It's a *SAML client* library, not a *SAML server*. As in - allows adding SAML single-sign-on to your ASP.NET app, but *not* to provide auth services to other apps. In other words, it's a library for "service-providers" not for "identity providers".
8 |
9 | ## Installation
10 |
11 | `Install-Package AspNetSaml`
12 |
13 | Adds a very small .NET Standard 2.0 library (11KB dll) that works with both ASP.NET Core and the "old" ASP.NET Framework. Please refer to [releases](https://github.com/jitbit/AspNetSaml/releases) for the change log.
14 |
15 | # Usage
16 |
17 | ## How SAML works? (please read this)
18 |
19 | SAML workflow has 2 steps:
20 |
21 | 1. User is redirected to the SAML provider (with some magic in the query-string) where he authenticates
22 | 2. User is redirected back to your app, where you validate the payload
23 |
24 | Here's how you do it (this example is for ASP.NET Core MVC):
25 |
26 | ## 1. Redirecting the user to the saml provider:
27 |
28 | ```c#
29 | //this example is an ASP.NET Core MVC action method
30 | public IActionResult Login()
31 | {
32 | //TODO: specify the SAML provider url here, aka "Endpoint"
33 | var samlEndpoint = "http://saml-provider-that-we-use.com/login/";
34 |
35 | var request = new AuthRequest(
36 | "http://www.myapp.com", //TODO: put your app's "entity ID" here
37 | "http://www.myapp.com/SamlConsume" //TODO: put Assertion Consumer URL (where the provider should redirect users after authenticating)
38 | );
39 |
40 | //now send the user to the SAML provider
41 | return Redirect(request.GetRedirectUrl(samlEndpoint));
42 | }
43 | ```
44 |
45 | ## 2. User has been redirected back
46 |
47 | User is sent back to your app - you need to validate the SAML response ("assertion") that you recieved via POST.
48 |
49 | Here's an example of how you do it in ASP.NET Core MVC
50 |
51 | ```c#
52 | //ASP.NET Core MVC action method... But you can easily modify the code for old .NET Framework, Web-forms etc.
53 | public async Task SamlConsume()
54 | {
55 | // 1. TODO: specify the certificate that your SAML provider gave you
56 | string samlCertificate = @"-----BEGIN CERTIFICATE-----
57 | BLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAH123543==
58 | -----END CERTIFICATE-----";
59 |
60 | // 2. Let's read the data - SAML providers usually POST it into the "SAMLResponse" var
61 | var samlResponse = new Response(samlCertificate, Request.Form["SAMLResponse"]);
62 |
63 | // 3. DONE!
64 | if (samlResponse.IsValid()) //all good?
65 | {
66 | //WOOHOO!!! the user is logged in
67 | var username = samlResponse.GetNameID(); //let's get the username
68 |
69 | //the user has been authenticated
70 | //now call context.SignInAsync() for ASP.NET Core
71 | //or call FormsAuthentication.SetAuthCookie() for .NET Framework
72 | //or do something else, like set a cookie or something...
73 |
74 | //FOR EXAMPLE this is how you sign-in a user in ASP.NET Core 3,5,6,7
75 | await context.SignInAsync(new ClaimsPrincipal(
76 | new ClaimsIdentity(
77 | new[] { new Claim(ClaimTypes.Name, username) },
78 | CookieAuthenticationDefaults.AuthenticationScheme)));
79 |
80 | return Redirect("~/");
81 | }
82 |
83 | return Content("Unauthorized");
84 | }
85 | ```
86 |
87 | # Bonus: reading more attributes from the provider
88 |
89 | SAML providers usually send more data with their response: username, first/last names etc. Here's how to get it:
90 |
91 | ```c#
92 | if (samlResponse.IsValid())
93 | {
94 | //WOOHOO!!! user is logged in
95 |
96 | //Some more optional stuff
97 | //let's extract username/firstname etc
98 | try
99 | {
100 | var username = samlResponse.GetNameID();
101 | var email = samlResponse.GetEmail();
102 | var firstname = samlResponse.GetFirstName();
103 | var lastname = samlResponse.GetLastName();
104 |
105 | //or read some custom-named data that you know the IdP sends
106 | var officeLocation = samlResponse.GetCustomAttribute("OfficeAddress");
107 | }
108 | catch (Exception ex)
109 | {
110 | //insert error handling code
111 | //in case some extra attributes are not present in XML, for example
112 | return null;
113 | }
114 | }
115 | ```
116 |
117 | # Notes about the source code
118 |
119 | All the functionality sits in one single short file [Saml.cs](https://github.com/jitbit/AspNetSaml/blob/master/AspNetSaml/Saml.cs) other stuff in this repo are just unit tests, nuget-packaging etc. You can take that file and throw it in your project, it should work just fine.
120 |
121 | P.S. This library has been battle-tested for years in production in our [helpdesk app](https://www.jitbit.com/helpdesk/) please check it out if you're looking for a ticketing system for your team. Cheers.
122 |
--------------------------------------------------------------------------------
/testenvironments.json:
--------------------------------------------------------------------------------
1 | //this file enables debugging unit-tests on WSL2 ubuntu when working on Windows
2 | {
3 | "version": "1",
4 | "environments": [
5 | {
6 | "name": "WSL-Ubuntu",
7 | "type": "wsl",
8 | "wslDistribution": "Ubuntu"
9 | }
10 | ]
11 | }
--------------------------------------------------------------------------------