Novolis Docs
novolis-security / owasp-security-evaluation.md

OWASP security evaluation — novolis-security

dotnetsecuritynovolis
FieldValue
Date2026-09-30 (logger sinks, idle timeout, grant revoke, live key rotate, form_post)
SupersedesEarlier 2026-09-30 sheet that still scored idle timeout, grant revoke, live rotation, form_post, and logging as Partial
Targetnovolis-security libraries: Authentication, OAuth, Authorization, password hashing, encryption, cryptography, secrets, HIBP, SecureText
Version evaluatedWorking tree on main after ILogger event sinks as default plus idle timeout, RevokeGrantsAsync, SigningKeyRing.RotateAsync, and response_mode=form_post
EvaluatorMaintainer review plus the automated suites below
Security checkPassed (selected ASVS 5.0.0 Level 2 library controls)
OverallMFA is a product plug-in (IMfaProvider, default NoopMfaProvider). Sign-in failures share a cache-backed counter and disable the credential when the budget is spent. ICacheStore is in-memory for tests and a single process; a farm replaces it with a distributed store. Authentication and OAuth events default to ILogger.

This is a library evaluation of a token mint and tenant authorization framework, not a hosted Identity Provider pentest and not an evaluation of Duende IdentityServer. TLS, edge WAF, key custody, consent UI, and the concrete MFA method (TOTP, WebAuthn, SMS) live in the executable host. See what-this-is.md.

1. Scope

In scope

  • Novolis.Security.Authentication* — identifier directory, isolated Argon2id credentials, browser sessions
  • Novolis.Security.OAuth* — Authorization Code + S256 PKCE, client credentials, rotating refresh tokens, ES384 access tokens with cnf, DPoP, revoke, discovery, JWKS
  • Novolis.Security.Authorization* — tenant-scoped default-deny permissions
  • Novolis.Security.PasswordHashing — Argon2id PHC
  • Novolis.Security.Encryption — AES-256-GCM
  • Novolis.Security.Cryptography — CSPRNG, fixed-time compare, HKDF-SHA512
  • Novolis.Security.Secrets / WordLists
  • Novolis.Security.HaveIBeenPwned — Pwned Passwords range API as IPasswordBreachChecker
  • Novolis.Security.SecureText — P-256 bundles, ECDH, HKDF-SHA256, AES-256-GCM

Out of scope

  • OpenID Connect, SAML, dynamic client registration, PAR, JAR
  • Duende IdentityServer / Keycloak / Auth0 / Entra feature parity (this library is a token mint + tenant authz, not an IdP)
  • A built-in TOTP/WebAuthn/SMS implementation (the host supplies IMfaProvider)
  • Host TLS, HSTS, WAF, HSM, SIEM

2. Methodology

Controls are scored only where they apply to a library.

  1. OWASP ASVS 5.0.0 (May 2025) — V6 authentication, V7 session, V8 authorization, V9 self-contained tokens, V10 OAuth, V11 cryptography, V14 data protection, V16 logging. Level 2 is the bar for the identity core. Level 3 items are recorded; DPoP (10.4.14) is implemented. MFA (6.3.3) is a product IMfaProvider.
  2. OWASP API Security Top 10 (2023).
  3. Cheat sheets: Password Storage, Authentication, Cryptographic Storage, JSON Web Token.
  4. OAuth 2.0 Security BCP (RFC 9700), RFC 9449 DPoP, RFC 7638 JWK thumbprint, and RFC 7009 revocation.
ScoreMeaning
PassImplemented and covered by a test, or true by construction
PartialImplemented with a residual risk called out in the findings
FailMissing or unsafe in the library itself
N/AHost, product, or out of scope

3. Executive summary

The morning 2026-09-30 sheet is stale. The gaps it locked in as current behavior are remedied:

  • RegisterAsync rejects passwords shorter than 8 characters even if options try to lower the floor. IPasswordBreachChecker is required outside Development. Have I Been Pwned implements the checker; Authentication still does not reference that assembly.
  • Sign-in failures increment ICacheStore (auth:fail:cred:{reference}). At MaxSignInFailures the credential is disabled and later attempts dummy-verify and fail closed, including the correct password. Unknown identifiers share an identifier-hash counter and the same invalid_credentials error.
  • IMfaProvider runs after a correct password. The default is NoopMfaProvider. A product provider uses the same cache for challenges and one-time proofs. Registration does not require MFA (enrollment is a product step).
  • ICacheStore lives on Authentication abstractions so Authentication and OAuth share one product cache. In-memory (IsProcessLocal) is for tests and a single process. A farm replaces it with Redis (or similar). TryCreateAsync leases refresh rotation and code consume against that same store.
  • A second sign-in revokes every other live session. DisableAsync sets IdentityRecord.Disabled, revokes sessions, and notifies revocation sinks. Authorization denies a disabled identity before role expansion.
  • SignOutAsync and DisableAsync invoke IIdentityRevocation. OAuth revokes every refresh family for that identity and writes an identity not-before into ICacheStore. Access tokens with iat at or before that cutoff fail ValidateAsync.
  • Authorization-code replay returns invalid_grant and revokes the access-token jti and the refresh family recorded on the consumed code.
  • Refresh rotation copies FamilyExpiresUtc from first issue and clamps ExpiresUtc. A refresh at or after the family cap is invalid_grant.
  • Token attempts are limited per client_id and per remote IP. TrustForwardedFor is false by default.
  • Issuance requires a DPoP ES256 proof (typ dpop+jwt) or an mTLS certificate thumbprint. Access tokens carry cnf.jkt or cnf.x5t#S256. ValidateAsync rejects a confirmation-bound token without a matching proof.
  • Authentication and OAuth event sinks default to ILogger (LoggerAuthenticationEventSink, LoggerEventStore). A missing ILoggerFactory uses NullLogger. AddNovolisOAuthEvents replaces the OAuth logger with a host delegate. Production still throws on NoopEventStore and in-memory stores unless AllowInMemoryStores is set.
  • Browser sessions have a 20-minute idle timeout (SessionIdleTimeout) on ICacheStore in addition to the 8-hour absolute lifetime.
  • RevokeGrantsAsync is the library API for “sign out everywhere”: it revokes every browser session and notifies IIdentityRevocation (refresh families and access-token not-before). Product UIs bind to it; this library does not ship a screen.
  • SigningKeyRing.RotateAsync installs a new ES384 current signer and keeps previous keys for validation and JWKS.
  • /oauth/authorize accepts response_mode=form_post (auto-submit POST of code/state). Default remains query redirect. Discovery lists query and form_post.

What was already holding up is unchanged: Argon2id, AES-256-GCM, ES384-only JWT validation, exact redirect allow-list, S256 PKCE, credential/identity file isolation, default-deny tenant authorization.

4. Threat model

AssetAttackerImpact if lost
Argon2id password hashDatabase dumpOffline cracking. The hash is not stored beside the email
Client secret PBKDF2-SHA512 hashDatabase dumpOffline guessing of a low-entropy secret the host chose
Refresh token {id}.{secret}TheftNew access tokens until rotation, reuse detection, family revoke, family cap, or identity sign-out
Authorization code {id}.{secret} plus PKCE verifierInterceptionOne redemption. Replay revokes the first access jti and refresh family
ECDSA P-384 private PEMHost compromiseForge access tokens for this issuer
Access JWTTheftAPI access until exp only if the thief also has the DPoP key or matching client certificate
Token endpointOnline guessingArgon2 or PBKDF2 cost plus per-client_id and per-IP windows
Sign-inOnline guessingDummy Argon2 on misses, per-credential failure budget, credential disable

Assumed host: HTTPS only, PEM supplied by the host, distributed ICacheStore when more than one process is used, IPasswordBreachChecker registered outside Development, real IMfaProvider when a second factor is required.

5. ASVS 5.0.0 (Level 2, selected)

V6 — Authentication

IDControlScoreEvidence
6.2.1User passwords at least 8 charactersPassRegisterAsync rejects "short" / "x" with password_too_short. The floor is 8 even if options try to lower it
6.2.5No composition rulesPassThe hasher and registrar do not demand character classes
6.2.8No truncation or case foldingPassOver-long passwords fail verify. NFC is applied on hash and verify. Case is preserved
6.2.9At least 64 characters permittedPassMaxPasswordLength default 1024
6.2.10No periodic password rotationPassThe library does not expire passwords
6.2.4 / 6.2.12Denylist and breached-password checkPassIPasswordBreachChecker is called on register and password change. A missing checker throws outside Development. HIBP implements the checker in a separate assembly. Checker exceptions fail closed (password_check_unavailable)
6.2.2 / 6.2.3Users can change password with current and newPassChangePasswordAsync dummy-verifies the current password, applies the same policy as register, replaces the hash, and revokes sessions and OAuth grants
6.2.11Context-specific forbidden fragmentsPassIdentifier substring and ForbiddenPasswordFragments fail with password_forbidden
6.3.1Anti-automationPassToken endpoint: 30 attempts / minute / client_id and the same window per IP. Sign-in: MaxSignInFailures (default 5) on ICacheStore; at the cap the credential is disabled and later attempts fail closed with dummy Argon2
6.3.2No default accountsPassNo seeded root or admin credential
6.3.3MFA or combined factorsPassIMfaProvider after a correct password. Default NoopMfaProvider for tests and single-factor hosts. Products replace it and use ICacheStore for challenges. A host that keeps the no-op on the public internet is choosing not to enforce a second factor
6.3.4Consistent pathwaysPassPassword, implicit, and device grants are unsupported_grant_type. Public clients cannot use client credentials
6.4.2No password hints or secret questionsPassCredentialRecord and IdentityRecord have no hint or KBA fields
6.5.1MFA proofs one-timePassProduct IMfaProvider uses ICacheStore.TryCreateAsync so a proof cannot be replayed. Demonstrated by SignIn_ProductMfa_RequiresProof_AndConsumesItOnce

V7 — Session management

IDControlScoreEvidence
7.2.1Backend verificationPassSession id is an opaque reference looked up server-side
7.2.2Dynamic tokensPassSessions, codes, and refresh secrets are generated per issuance
7.2.3CSPRNG, at least 128 bitsPassSession id is 32 bytes from RandomNumberGenerator (asserted). Code and refresh secrets are 32 bytes
7.2.4New session on authentication, previous session terminatedPassA second sign-in returns a different session id. The first session no longer authenticates
7.3TimeoutsPassSession default 8 hours absolute plus 20-minute idle (SessionIdleTimeout on ICacheStore, refreshed on each successful lookup). Access 15 minutes. Authorization code 2 minutes. Refresh family cap is RefreshTokenLifetime from first issue
7.4.1Logout blocks further usePassSignOutAsync revokes that session id, every refresh family for the identity, and access tokens whose iat is at or before the identity cutoff
7.4.2Disable or delete ends sessionsPassDisableAsync sets Disabled, revokes every session, notifies revocation sinks. GetAuthenticatedIdentityAsync returns null. Authorization denies before role expansion. Code redemption is invalid_grant

V8 — Authorization

IDControlScoreEvidence
8.2.1Function-level allow-listPassDefault deny. A role assignment is required. Unknown grants and unknown scopes fail closed. Disabled identities are denied even with a role
8.2.2Object-level / BOLAPassRefresh and revoke bind to the authenticated client. Role assignments do not cross tenants. Authorization codes are bound to client_id
8.2.3Field-level / BOPLAPassJWT sub is the identity id. Email and password are absent. Token requests that are not form bodies are rejected
8.3Operation-levelPassClient credentials omit refresh tokens and have a null identity. Public clients are unauthorized_client for that grant. Client-credentials subjects are client ids and are unchanged by user sign-out

V9 — Self-contained tokens

IDControlScoreEvidence
9.1.1Signature checked before usePassTampered tokens fail ValidateAsync
9.1.2Algorithm allow-list, no nonePassValidAlgorithms is ES384 only. alg=none and HS256 are rejected
9.1.3Keys from the issuer, not jku / jwkPassValidation uses the process key ring
9.2.1nbf and expPassBoth required. Default clock skew 30 seconds. Identity not-before is an extra cutoff
9.2.3Audience allow-listPassaud must match OAuthOptions.Audiences
9.2.2Token type and purposePassAccess tokens use typ at+jwt. ValidateAsync rejects other types. DPoP proofs require typ dpop+jwt

V10 — OAuth and OIDC

IDControlScoreEvidence
10.4.1Exact redirect allow-listPassUnregistered, prefix, and query-appended redirect URIs return 400 and do not redirect
10.4.2Code single-use, and replay revokes issued tokensPassSecond redemption is invalid_grant. The access token from the first redemption fails ValidateAsync. The refresh family is revoked
10.4.3Code lifetime ≤ 10 minutesPassDefault AuthorizationCodeLifetime is 2 minutes
10.4.4No implicit, no password grantPassresponse_type other than code is 400. Discovery omits password and implicit
10.4.5Refresh rotation and reuse detectionPassRotation invalidates the presented token. Reuse revokes the family. ICacheStore.TryCreateAsync leases rotation and code consume. In-memory is process-local by design; a farm supplies a distributed ICacheStore
10.4.6PKCE S256, reject plainPassMissing or plain challenges fail. The token request requires code_verifier
10.4.7Dynamic client registrationN/ANot implemented
10.4.8Absolute refresh expirationPassFamilyExpiresUtc is set at first issue to now + RefreshTokenLifetime, copied on rotation, and clamps ExpiresUtc. A refresh at or after the cap is invalid_grant
10.4.9User can revoke refresh tokensPass/oauth/revoke revokes the family after client authentication. SignOutAsync, DisableAsync, and RevokeGrantsAsync revoke every family for the identity and write an access-token not-before. Product UIs bind “sign out everywhere” to RevokeGrantsAsync; this library does not ship a screen
10.4.10Confidential client authenticationPassSecret required. Basic and form client_id mismatch is invalid_client. Unknown and bad secrets share that error. A public client that presents a secret is rejected
10.2.1PKCE or state against CSRFPassPKCE S256 is mandatory. state is echoed when present and is not required
10.4.14Sender-constrained access tokensPassDPoP ES256 proofs or cnf.x5t#S256 from a client certificate. Issuance with neither fails. ValidateAsync requires a matching proof and ath. Mismatch and missing proof fail
10.3Resource server uses token claimsPassValidateAsync requires aud, exposes sub, scope, and client_id, and requires a matching DPoP or certificate confirmation

OIDC client and identity-provider sections are N/A. Discovery does not advertise a userinfo endpoint, and /.well-known/openid-configuration is absent unless the host opts into the alias.

V11 — Cryptography

IDControlScoreEvidence
11.3.2Approved cipherPassAES-256-GCM for string encryption and SecureText
11.3.3Authenticated encryptionPassGCM tag. Tampered ciphertext and tampered AAD fail
11.4Password hashingPassArgon2id PHC. No MD5 or SHA-1 password storage. Verify refuses oversized m / t / p
11.4Client secretsPassPBKDF2-HMAC-SHA512, 210000 iterations, 16-byte salt, fixed-time compare
11.5.1CSPRNG for non-guessable valuesPassRandomNumberGenerator via SecureRandom. Not System.Random
11.2Key inventory and agilityPassAlgorithms are explicit. The host supplies PEM. SigningKeyRing.RotateAsync installs a new current signer and keeps previous keys for validation and JWKS. Ephemeral P-384 is refused outside Development

HIBP uses SHA-1 because the Pwned Passwords range API defines the prefix that way. That hash is not a password store.

SecureText derives a pairwise AES-256 key with P-256 ECDH and HKDF-SHA256. Bundle signatures are checked before use.

V14 — Data protection

IDControlScoreEvidence
14.2.1Secrets not in the query stringPassThe token endpoint is POST form only. GET /oauth/token is 405. Authorization default is query (RFC 6749). response_mode=form_post returns an auto-submit POST of code/state. Discovery lists query and form_post
14.2.2Sensitive responses not cachedPassToken and revoke responses set Cache-Control: no-store and Pragma: no-cache
14.2.6Minimum dataPassCredential row is reference, hash, disabled, timestamps. Identity row holds the email and no password hash. JSON files do not combine the two

V16 — Logging

IDControlScoreEvidence
16.2.5No credentials in logsPassSecurity events carry type, client id, identity id, grant, and error. They do not carry passwords, refresh secrets, or PEMs
16.3.1Authentication outcomes loggedPassDefault sinks are LoggerAuthenticationEventSink and LoggerEventStore (ILogger). Payloads are type, client id, identity id, grant, and error. A missing logger factory uses NullLogger. Production still refuses an explicit NoopEventStore unless AllowInMemoryStores
16.3.2Failed authorization attempts loggedPassDefault LoggerAuthorizationEventSink. Denies record type, tenant, identity, and permission id
16.5.1Generic errorsPassToken errors are JSON error codes. They do not include stack traces, PEMs, or secrets

6. OWASP API Security Top 10 (2023)

APIThemeScoreNotes
API1Broken object level authorizationPassClient binding on refresh, revoke, and authorization codes. Tenant binding on roles
API2Broken authenticationPassStrong token crypto, DPoP, registration policy, no password grant, sign-in lockout, MFA hook
API3Broken object property level authorizationPassNo email in the JWT. JSON bodies on the token endpoint are rejected
API4Unrestricted resource consumptionPassArgon2 and PHC caps, authorization-code and password length caps, per-client_id and per-IP windows, per-credential sign-in budget
API5Broken function level authorizationPassDefault deny. Unsupported grants fail closed. Public clients cannot take client credentials. Disabled identities cannot use assigned roles
API6Unrestricted sensitive flowsPassThe token endpoint limiter is client_id and IP. Rotating client_id from one address is still temporarily_unavailable
API7SSRFPassNo URL fetch from client input. jku is not followed
API8Security misconfigurationPassEphemeral signing keys are Development-only. In-memory stores and the no-op sink throw outside Development unless AllowInMemoryStores
API9Improper inventoryPassDiscovery lists authorization code, client credentials, refresh, code, and S256. It is not an OpenID Provider
API10Unsafe API consumptionPassHIBP uses k-anonymity. Range lookups throw unless the origin is https://api.pwnedpasswords.com

7. Cheat sheets and RFC 9700

ItemScore
Argon2id, 19 MiB, t=2, p=1, unique 16-byte salt, PHCPass
Unicode NFC before hash and verifyPass
Verify-time caps (m ≤ 65536 KiB, t ≤ 12, p ≤ 16)Pass
PepperN/A — host KMS if added
Deny alg=none and HMAC/RSA confusionPass
JWKS without private dPass
Short access lifetime (15 minutes)Pass
Avoid ROPC and implicitPass
Exact redirect URIPass
PKCE S256Pass
Refresh rotation and reuse detectionPass in one process with in-memory cache. Pass across processes when the product registers a distributed ICacheStore
Refresh cannot expand scope or audiencePass
Issuer from options, not the Host headerPass
Sender-constrained access tokensPass (DPoP ES256 or mTLS thumbprint)
Client authentication, Basic exclusive when presentPass

8. Findings

Closed: F-02 — cache is a product implementation

  • Status: Closed as a library gap. Documented as a host choice.
  • What: ICacheStore is the shared product cache for lockout, MFA, rate limits, identity not-before, and refresh/code leases. InMemoryCacheStore.IsProcessLocal is true: that is correct for tests and a single process. A farm replaces the registration with a distributed backing store so every process sees the same counters and once-only gates. Repository Lock stays process-local because IRepository has no compare-and-swap; the lease on ICacheStore is the cross-process gate.

Closed on the identity-core pass

IdStatus
F-03 IP attempt limitClosed. oauth:rate:ip:{address} uses the same window as the client-id counter. TrustForwardedFor is false by default; when set, only the left-most X-Forwarded-For is used
F-07 sender-constrained tokensClosed. DPoP ES256 subset of RFC 9449, or cnf.x5t#S256 from a client certificate. Stolen bearer use without the proof fails
F-08 in-memory production storesClosed. Outside Development, process-local cache (IsProcessLocal) / in-memory client/code/refresh stores or an explicit NoopEventStore throw unless AllowInMemoryStores. The default event sink is ILogger, not no-op
F-11 registration policyClosed. 8-character floor and IPasswordBreachChecker. Authentication does not reference HaveIBeenPwned
F-12 sessions and disableClosed. Sign-in revokes other sessions. DisableAsync revokes sessions and sinks. Authorization denies disabled identities. A missing identity record still authorizes from roles
F-13 sign-out tokensClosed. IIdentityRevocation revokes refresh families and writes an identity not-before. Client-credentials tokens whose subject is the client id are unchanged
F-14 code replayClosed. Replay returns the consumed row, revokes that family, and denies that jti until exp
F-15 family capClosed. FamilyExpiresUtc is set once at first issue and copied on rotation

Closed since 2026-09-22

IdStatus
F-01 password grantClosed. grant_type=password is unsupported_grant_type
F-04 unknown scopesClosed. invalid_scope
F-05 HIBP range-body loggingClosed. Debug log is the suffix count
F-06 Unicode NFCClosed. Hash and verify both normalize Form C
F-09 disabled clientsClosed. invalid_client
F-10 SQLite OAuthClientClosed. StoredOAuthClient packs lists. The SQLite reference scenario persists the client and completes the code flow

9. Tests that back this sheet

Reference host: tests/Novolis.Security.OAuth.Integration/ReferenceIdentityHost.cs.

CheckWhere
JSON and SQLite: isolated credential file, 256-bit session, short-password reject, session replace, DPoP-bound code + S256, replay invalidates access token, sign-out invalidates JWT, disable vs redeem and authorizationOwaspReferenceScenarioTests
Short password, breached password, failure lockout, product MFA, logger defaults, idle timeout, RevokeGrantsAsync, password change, forbidden fragments, no KBA fields, production breach-checker guardAuthenticationIsolationTests
Live signing-key rotate keeps previous kid valid; RevokeGrantsAsync invalidates access and refresh; typ at+jwt; resource-server sub/scope/audOAuthTokenServiceTests
Open redirect, prefix and query redirect, implicit response_type, optional state, form_post encoding, GET token 405, generic token errors, no-store, Host header, per-client and per-IP rate limit, production in-memory guard, DPoP mismatchOwaspHttpScenarioTests
Denied authorization is recorded; default authorization sink is ILoggerAuthorizationEngineTests
Pwned Passwords origin pinHaveIBeenPwnedClientTest
Refresh family absolute capOAuthTokenServiceTests
Authentication does not reference HIBP; OAuth and Authorization do not reference each otherSecurityArchitectureTests
alg=none, HS256, concurrent refreshOAuthRedTeamTests
Password grant rejected, code replay, wrong verifier, scope and audience escalation, JWKS without dOAuthTokenServiceTests, OAuthAttackSurfaceTests
Argon2 NFC, PHC bombs, AES-GCM tamper, SecureText AAD and bundle signatureHasher, encryptor, and SecureText tests
Default deny, group grant, composite roles, cross-tenantAuthorizationEngineTests

10. Host production checklist

  1. HTTPS only, with HSTS at the edge.
  2. Set OAuthOptions.Issuer to the public HTTPS origin and set Audiences to the resource servers.
  3. Provide a P-384 PKCS#8 PEM. Do not set AllowEphemeralSigningKey outside Development.
  4. Replace in-memory stores. JSON and SQLite both complete the reference scenario. Do not set AllowInMemoryStores in production.
  5. Call AddNovolisPasswordBreachCheck() (or another IPasswordBreachChecker) outside Development.
  6. Keep the default ILogger sinks, or call AddNovolisOAuthEvents (or another IEventStore) if the host wants a custom observer. Do not register NoopEventStore in production.
  7. Keep the identifier directory off the credential records.
  8. Put a trusted reverse proxy in front if you set TrustForwardedFor. The library uses only the left-most X-Forwarded-For value.
  9. Keep in-memory ICacheStore for tests and a single process. Replace it with a distributed cache before more than one process, pod, or app shares lockout, MFA, or OAuth leases.
  10. Present DPoP proofs (or an mTLS client certificate) on token requests and resource requests.
  11. Keep NoopMfaProvider only where a second factor is not required. Internet issuers replace IMfaProvider.

11. Verdict

AreaVerdict
Password hashingPass
String encryption and SecureTextPass
JWT mint and validatePass for ES384 with cnf
Authorization Code + S256 PKCEPass, including replay revocation
Refresh tokensPass; distributed ICacheStore when more than one process
Redirect bindingPass
Store isolationPass
Registration policyPass
Session termination on re-auth, sign-out, and disablePass
Sign-in lockoutPass
MFAPass as a product IMfaProvider (default no-op)
Sender-constrained access tokensPass (DPoP or certificate thumbprint)
SQLite as a client storePass
Fit as an OpenID ProviderNo
Fit as Duende IdentityServer / commercial IdPNo

Security check: Passed. Selected ASVS 5.0.0 Level 2 library controls for this identity core are met. A hosted product still needs TLS, a distributed cache when it scales out, a registered breach checker outside Development, and a real IMfaProvider when a second factor is required. Event sinks already default to ILogger.

Re-run:

dotnet test d:\novolis\novolis-security\tests\Novolis.Security.Unit\Novolis.Security.Unit.csproj -p:NovolisUseProjectReferences=true
dotnet test d:\novolis\novolis-security\tests\Novolis.Security.OAuth.Integration\Novolis.Security.OAuth.Integration.csproj -p:NovolisUseProjectReferences=true

Unit: 95 passed, 1 skipped network check. Integration: 4 passed.

12. References

  • OWASP ASVS 5.0.0
  • OWASP API Security Top 10 2023
  • OWASP Password Storage, Authentication, Cryptographic Storage, and JWT cheat sheets
  • RFC 6749, RFC 7009, RFC 7636, RFC 7638, RFC 8414, RFC 9449, RFC 9700
  • design.md
  • what-this-is.md