| | | 1 | | using Microsoft.IdentityModel.JsonWebTokens; |
| | | 2 | | |
| | | 3 | | namespace ArturRios.Heimdall.WebApi.Security; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Whether a token names Heimdall as its issuer and one of the audiences Heimdall issues it |
| | | 7 | | /// under — the check every reader of a Heimdall token makes beside the signature and lifetime. |
| | | 8 | | /// </summary> |
| | | 9 | | /// <remarks> |
| | | 10 | | /// <para> |
| | | 11 | | /// <b>Why it exists.</b> <c>ArturRios.Jwt</c> checks only the signature and lifetime, and a |
| | | 12 | | /// signature does not say whose token it is. Heimdall's ES256 key signs every kind of token it |
| | | 13 | | /// issues, so a valid signature alone would let one kind pass for another (Threat Model TB-5). |
| | | 14 | | /// Each reader names the audiences it takes: the bearer-token validator the three person |
| | | 15 | | /// audiences (<see cref="PersonTokenAudiences" />), the challenge reader the challenge audience |
| | | 16 | | /// alone, and the application-token reader the application audience alone. |
| | | 17 | | /// </para> |
| | | 18 | | /// <para> |
| | | 19 | | /// <b>Exact.</b> The issuer must equal the configured one, ordinally; a token without an |
| | | 20 | | /// <c>iss</c> passes only when none is configured, which is how Heimdall then issues. A token |
| | | 21 | | /// must carry exactly one audience, because Heimdall never issues one with several; no |
| | | 22 | | /// <c>aud</c> at all counts as the empty audience, again how Heimdall issues when none is |
| | | 23 | | /// configured. Run it only on a token whose signature has been checked: before that, the |
| | | 24 | | /// claims are whatever the sender wrote. |
| | | 25 | | /// </para> |
| | | 26 | | /// </remarks> |
| | | 27 | | public static class TokenOrigin |
| | | 28 | | { |
| | | 29 | | /// <summary> |
| | | 30 | | /// Whether <paramref name="token" /> was issued by <paramref name="issuer" /> for exactly one |
| | | 31 | | /// of <paramref name="audiences" />. An unreadable token is not. |
| | | 32 | | /// </summary> |
| | | 33 | | public static bool IsIssuedFor(string token, string issuer, params IReadOnlyCollection<string> audiences) |
| | | 34 | | { |
| | | 35 | | try |
| | | 36 | | { |
| | 1102 | 37 | | var jwt = new JsonWebToken(token); |
| | 1099 | 38 | | var tokenAudiences = jwt.Audiences.ToList(); |
| | | 39 | | |
| | 1099 | 40 | | var audience = tokenAudiences.Count switch |
| | 1099 | 41 | | { |
| | 1 | 42 | | 0 => string.Empty, |
| | 1095 | 43 | | 1 => tokenAudiences[0], |
| | 3 | 44 | | _ => null |
| | 1099 | 45 | | }; |
| | | 46 | | |
| | 1099 | 47 | | return audience is not null && |
| | 1099 | 48 | | string.Equals(jwt.Issuer ?? string.Empty, issuer, StringComparison.Ordinal) && |
| | 1099 | 49 | | audiences.Contains(audience, StringComparer.Ordinal); |
| | | 50 | | } |
| | 3 | 51 | | catch (Exception) |
| | | 52 | | { |
| | | 53 | | // Total, like the claims reader: a token whose header, issuer or audience cannot be read |
| | | 54 | | // is refused as unauthenticated, never failed with a 500. |
| | 3 | 55 | | return false; |
| | | 56 | | } |
| | 1102 | 57 | | } |
| | | 58 | | } |