Skip to content

Commit d56d502

Browse files
authored
Merge pull request #1321 from ITfoxtec/test
Test
2 parents ffa51b1 + 27a61aa commit d56d502

23 files changed

+706
-58
lines changed

.github/workflows/sbom_syft.yaml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: sbom_syft
2+
3+
on:
4+
push:
5+
branches:
6+
- "main"
7+
workflow_dispatch:
8+
9+
jobs:
10+
sbom:
11+
name: Generate SBOM with Syft
12+
runs-on: ubuntu-latest
13+
permissions:
14+
contents: write
15+
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Prepare SBOM directory
21+
run: mkdir -p sbom
22+
23+
- name: Generate SPDX SBOM
24+
uses: anchore/sbom-action@v0
25+
with:
26+
path: .
27+
format: spdx-json
28+
output-file: sbom/foxids.spdx.json
29+
upload-artifact: false
30+
31+
- name: Generate CycloneDX SBOM
32+
uses: anchore/sbom-action@v0
33+
with:
34+
path: .
35+
format: cyclonedx-json
36+
output-file: sbom/foxids.cdx.json
37+
upload-artifact: false
38+
39+
- name: Upload SBOM artifacts
40+
uses: actions/upload-artifact@v4
41+
with:
42+
name: foxids-sbom
43+
path: sbom
44+
if-no-files-found: error

docs/app-reg-saml-2.0.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SAML 2.0 application registration
1+
# SAML 2.0 application registration
22

33
FoxIDs SAML 2.0 application registration enable you to connect an SAML 2.0 based application.
44

@@ -12,6 +12,8 @@ Your application become a SAML 2.0 Relying Party (RP) and FoxIDs acts as an SAML
1212

1313
FoxIDs support [SAML 2.0 redirect and post bindings](https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf).
1414

15+
FoxIDs also supports forwarding a login hint from the SAML Authn request URL using either the login_hint or LoginHint query parameter when the request does not include a NameID. This enables relying parties such as Microsoft Entra and Okta to pre-fill the user identifier in the FoxIDs login experience.
16+
1517
A application registration expose [SAML 2.0 metadata](https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf) where your application can discover the SAML 2.0 Identity Provider (IdP).
1618

1719
Both the login, logout and single logout [SAML 2.0 profiles](https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf) are supported. The Artifact profile is not supported.
@@ -69,3 +71,5 @@ The `AuthnContextClassRef` property can be set in the `Login` method in `SamlCon
6971

7072
return binding.Bind(saml2AuthnRequest).ToActionResult();
7173
}
74+
75+

docs/claim-transform-task.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,4 @@ Use a `Regex map` claim transformation and select the `Replace claim` action.
195195

196196
Find the ID without the default added post authentication method name with regex `^(nemlogin\|)(?<map>.+)$`
197197

198-
> You can do the same in a SAML 2.0 authentication method using the `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier` claim instead of the `sub` claim.
198+
> You can do the same in a SAML 2.0 authentication method using the `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier` claim (which contains the SAML 2.0 Authn Response `NameID` value) instead of the `sub` claim.

docs/name-title-icon-css.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Find the login authentication method in [FoxIDs Control Client](control.md#foxid
2626

2727
1. Add the **Browser Title** text
2828
2. Add the **Browser Icon URL** from an external site, supported image formats: ico, png, gif, jpeg and webp
29+
You can also paste an inline data URI such as `data:image/png;base64,...` to embed the icon directly.
2930
3. Add your **CSS**, if necessary drag the field bigger
3031
4. Click **Update**
3132

@@ -71,6 +72,18 @@ It is also possible to use a logo image.
7172
}
7273
```
7374

75+
You can also embed the logo image directly in the CSS with a `data:` URI to avoid external requests.
76+
77+
```CSS
78+
.brand-content-text {
79+
display: none;
80+
}
81+
82+
.brand-content-icon::before {
83+
content:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9FHSMNcAAAAASUVORK5CYII=');
84+
}
85+
```
86+
7487
Add a background image from an external site.
7588

7689
```CSS
@@ -144,4 +157,4 @@ div.page-content::before {
144157
```
145158

146159

147-
![Configure login box with CSS](images/configure-login-css-sample-test.png)
160+
![Configure login box with CSS](images/configure-login-css-sample-test.png)

src/FoxIDs.Control/FoxIDs.Control.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFramework>net9.0</TargetFramework>
5-
<Version>2.5.3</Version>
5+
<Version>2.5.4</Version>
66
<RootNamespace>FoxIDs</RootNamespace>
77
<Authors>Anders Revsgaard</Authors>
88
<Company>FoxIDs</Company>

src/FoxIDs.Control/Logic/Validators/ValidateApiModelExternalLoginPartyLogic.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ public bool ValidateApiModel(ModelStateDictionary modelState, Api.ExternalLoginU
3232
party.Css = sanitizedCss;
3333
}
3434

35-
if (!ValidateApiModelLoginPartyLogic.TryValidateIconUrl(modelState, logger, nameof(Api.ExternalLoginUpParty.IconUrl), party.IconUrl))
35+
if (!ValidateApiModelLoginPartyLogic.TryValidateIconUrl(modelState, logger, nameof(Api.ExternalLoginUpParty.IconUrl), party.IconUrl, out var sanitizedIconUrl))
3636
{
3737
isValid = false;
3838
}
39+
else
40+
{
41+
party.IconUrl = sanitizedIconUrl;
42+
}
3943

4044
if (party.Title.IsNullOrWhiteSpace())
4145
{

src/FoxIDs.Control/Logic/Validators/ValidateApiModelLoginPartyLogic.cs

Lines changed: 110 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using FoxIDs.Infrastructure;
22
using Api = FoxIDs.Models.Api;
33
using ITfoxtec.Identity;
4+
using System;
5+
using System.Collections.Generic;
46
using Microsoft.AspNetCore.Http;
57
using Microsoft.AspNetCore.Mvc.ModelBinding;
68
using System.ComponentModel.DataAnnotations;
@@ -19,7 +21,17 @@ public class ValidateApiModelLoginPartyLogic : LogicBase
1921
private static readonly Regex cssStyleTagPattern = new Regex("<\\s*/?\\s*style[^>]*>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
2022
private static readonly Regex cssHtmlTagPattern = new Regex("<[^>]+>", RegexOptions.Compiled | RegexOptions.CultureInvariant);
2123
private static readonly Regex cssHtmlCommentPattern = new Regex("<!--.*?-->", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant);
22-
private static readonly Regex cssUnsafePattern = new Regex("(?i)(expression\\s*\\(|behavior(u)?r\\s*:|-moz-binding\\s*:|@import\\b|@charset\\b|@namespace\\b|url\\s*\\(\\s*[\'\\\"]?\\s*(?:javascript|vbscript|data)\\s*:|<\\s*/?\\s*(?:style|script)[^>]*>)", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant);
24+
private static readonly Regex cssUnsafePattern = new Regex("(?i)(expression\\s*\\(|behavior(u)?r\\s*:|-moz-binding\\s*:|@import\\b|@charset\\b|@namespace\\b|url\\s*\\(\\s*[\'\\\"]?\\s*(?:javascript|vbscript|data(?!\\s*:\\s*image\\/))\\s*:|<\\s*/?\\s*(?:style|script)[^>]*>)", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant);
25+
private const string DataUriPrefix = "data:";
26+
private static readonly IDictionary<string, string> iconExtensionToMimeType = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
27+
{
28+
[".ico"] = "image/x-icon",
29+
[".png"] = "image/png",
30+
[".gif"] = "image/gif",
31+
[".jpeg"] = "image/jpeg",
32+
[".webp"] = "image/webp"
33+
};
34+
private static readonly HashSet<string> supportedIconMimeTypes = new HashSet<string>(iconExtensionToMimeType.Values, StringComparer.OrdinalIgnoreCase);
2335

2436
private readonly TelemetryScopedLogger logger;
2537
private readonly ValidateApiModelGenericPartyLogic validateApiModelGenericPartyLogic;
@@ -45,10 +57,14 @@ public async Task<bool> ValidateApiModelAsync(ModelStateDictionary modelState, A
4557
party.Css = sanitizedCss;
4658
}
4759

48-
if (!TryValidateIconUrl(modelState, logger, nameof(Api.LoginUpParty.IconUrl), party.IconUrl))
60+
if (!TryValidateIconUrl(modelState, logger, nameof(Api.LoginUpParty.IconUrl), party.IconUrl, out var sanitizedIconUrl))
4961
{
5062
isValid = false;
5163
}
64+
else
65+
{
66+
party.IconUrl = sanitizedIconUrl;
67+
}
5268

5369
if (party.TwoFactorAppName.IsNullOrWhiteSpace())
5470
{
@@ -110,11 +126,6 @@ internal static bool TryValidateAndSanitizeCss(ModelStateDictionary modelState,
110126
throw new ValidationException("CSS contains unbalanced braces.");
111127
}
112128

113-
if (cssUnsafePattern.IsMatch(css))
114-
{
115-
throw new ValidationException("CSS contains unsupported or unsafe content.");
116-
}
117-
118129
sanitizedCss = SanitizeCss(css);
119130
return true;
120131
}
@@ -126,25 +137,26 @@ internal static bool TryValidateAndSanitizeCss(ModelStateDictionary modelState,
126137
}
127138
}
128139

129-
internal static bool TryValidateIconUrl(ModelStateDictionary modelState, TelemetryScopedLogger logger, string iconUrlFieldName, string iconUrl)
140+
internal static bool TryValidateIconUrl(ModelStateDictionary modelState, TelemetryScopedLogger logger, string iconUrlFieldName, string iconUrl, out string sanitizedIconUrl)
130141
{
142+
sanitizedIconUrl = iconUrl;
131143
if (iconUrl.IsNullOrWhiteSpace())
132144
{
133145
return true;
134146
}
135147

136148
try
137149
{
138-
var iconExtension = Path.GetExtension(iconUrl.Split('?')[0]);
139-
_ = iconExtension switch
150+
sanitizedIconUrl = iconUrl.Trim();
151+
sanitizedIconUrl = RemoveCssUrlWrapper(sanitizedIconUrl);
152+
if (IsDataImageIcon(sanitizedIconUrl))
140153
{
141-
".ico" => "image/x-icon",
142-
".png" => "image/png",
143-
".gif" => "image/gif",
144-
".jpeg" => "image/jpeg",
145-
".webp" => "image/webp",
146-
_ => throw new ValidationException($"Icon image format '{iconExtension}' not supported.")
147-
};
154+
ValidateDataImageIcon(sanitizedIconUrl);
155+
}
156+
else
157+
{
158+
ValidateIconUrlExtension(sanitizedIconUrl);
159+
}
148160

149161
return true;
150162
}
@@ -200,5 +212,86 @@ private static string RemoveUnsafeComments(string css)
200212
{
201213
return cssCommentPattern.Replace(css, match => cssUnsafePattern.IsMatch(match.Value) ? string.Empty : match.Value);
202214
}
215+
216+
private static string RemoveCssUrlWrapper(string iconUrl)
217+
{
218+
if (iconUrl.StartsWith("url(", StringComparison.OrdinalIgnoreCase) && iconUrl.EndsWith(")"))
219+
{
220+
var inner = iconUrl.Substring(4, iconUrl.Length - 5).Trim();
221+
inner = inner.Trim('\'', '"');
222+
return inner;
223+
}
224+
225+
return iconUrl;
226+
}
227+
228+
private static bool IsDataImageIcon(string iconUrl)
229+
{
230+
return iconUrl.StartsWith(DataUriPrefix, StringComparison.OrdinalIgnoreCase);
231+
}
232+
233+
private static void ValidateIconUrlExtension(string iconUrl)
234+
{
235+
var iconPath = iconUrl.Split(['?', '#'], 2)[0];
236+
var iconExtension = Path.GetExtension(iconPath);
237+
238+
if (iconExtension.IsNullOrWhiteSpace() || !iconExtensionToMimeType.ContainsKey(iconExtension))
239+
{
240+
throw new ValidationException($"Icon image format '{iconExtension}' not supported.");
241+
}
242+
}
243+
244+
private static void ValidateDataImageIcon(string iconUrl)
245+
{
246+
var commaIndex = iconUrl.IndexOf(',');
247+
if (commaIndex < 0)
248+
{
249+
throw new ValidationException("Icon data URI is missing image data.");
250+
}
251+
252+
var metadata = iconUrl.Substring(DataUriPrefix.Length, commaIndex - DataUriPrefix.Length);
253+
if (metadata.IsNullOrWhiteSpace())
254+
{
255+
throw new ValidationException("Icon data URI is missing the media type.");
256+
}
257+
258+
var metadataParts = metadata.Split(';', StringSplitOptions.RemoveEmptyEntries);
259+
var mimeType = metadataParts.FirstOrDefault()?.Trim();
260+
if (mimeType.IsNullOrWhiteSpace())
261+
{
262+
throw new ValidationException("Icon data URI is missing the media type.");
263+
}
264+
265+
if (!mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
266+
{
267+
throw new ValidationException($"Icon data URI media type '{mimeType}' is not an image.");
268+
}
269+
270+
if (!supportedIconMimeTypes.Contains(mimeType))
271+
{
272+
throw new ValidationException($"Icon image format '{mimeType}' not supported.");
273+
}
274+
275+
if (!metadataParts.Skip(1).Any(p => p.Equals("base64", StringComparison.OrdinalIgnoreCase)))
276+
{
277+
throw new ValidationException("Icon data URI must specify base64 encoding.");
278+
}
279+
280+
var imageData = iconUrl.Substring(commaIndex + 1).Trim();
281+
if (imageData.IsNullOrWhiteSpace())
282+
{
283+
throw new ValidationException("Icon data URI is missing image data.");
284+
}
285+
286+
try
287+
{
288+
Convert.FromBase64String(imageData);
289+
}
290+
catch (FormatException)
291+
{
292+
throw new ValidationException("Icon data URI image data is not valid base64.");
293+
}
294+
}
203295
}
204296
}
297+

src/FoxIDs.ControlClient/FoxIDs.ControlClient.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFramework>net9.0</TargetFramework>
5-
<Version>2.5.3</Version>
5+
<Version>2.5.4</Version>
66
<RootNamespace>FoxIDs.Client</RootNamespace>
77
<Authors>Anders Revsgaard</Authors>
88
<Company>FoxIDs</Company>

src/FoxIDs.ControlClient/Models/ViewModels/Parties/ExternalLoginUpPartyViewModel.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using FoxIDs.Infrastructure.DataAnnotations;
1+
using FoxIDs.Infrastructure.DataAnnotations;
22
using FoxIDs.Models.Api;
33
using System;
44
using System.Collections.Generic;
@@ -120,7 +120,7 @@ public class ExternalLoginUpPartyViewModel : IValidatableObject, IUpPartySession
120120
public string Title { get; set; }
121121

122122
[MaxLength(Constants.Models.LoginUpParty.IconUrlLength)]
123-
[Display(Name = "Browser Icon URL (https://example.somewhere/favicon.ico)")]
123+
[Display(Name = "Browser Icon URL (https://example.somewhere/favicon.ico or data:image/png;base64,...)")]
124124
public string IconUrl { get; set; }
125125

126126
[MaxLength(Constants.Models.LoginUpParty.CssStyleLength)]
@@ -190,4 +190,4 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
190190
return results;
191191
}
192192
}
193-
}
193+
}

src/FoxIDs.ControlClient/Models/ViewModels/Parties/LoginUpPartyViewModel.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public class LoginUpPartyViewModel : IUpPartySessionLifetime, IUpPartyHrd, IVali
154154
public string Title { get; set; }
155155

156156
[MaxLength(Constants.Models.LoginUpParty.IconUrlLength)]
157-
[Display(Name = "Browser Icon URL (https://example.somewhere/favicon.ico)")]
157+
[Display(Name = "Browser Icon URL (https://example.somewhere/favicon.ico or data:image/png;base64,...)")]
158158
public string IconUrl { get; set; }
159159

160160
[MaxLength(Constants.Models.LoginUpParty.CssStyleLength)]
@@ -252,4 +252,5 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
252252
return results;
253253
}
254254
}
255-
}
255+
}
256+

0 commit comments

Comments
 (0)