diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 5694956..06ea13f 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -12,10 +12,11 @@ The documentation uses [MkDocs](https://www.mkdocs.org/) and [Material for
MkDocs](https://squidfunk.github.io/mkdocs-material/). This documentation is both provided on readthedocs and built
in to the application itself.
-The application reads the configuration on start up and then populates an in-memory database with users and OpenID
-connect clients. Additionally, users and clients can be created at runtime through the web interface at `/users` and
-`/clients` respectively. Note that there is no persistence between application restarts, as the in-memory database is
-wiped and a new one is used - any users or clients created at runtime will be lost upon restart.
+The application reads the configuration on start up and then populates the database with users and OpenID connect
+clients. By default, an in-memory database is used and all data is lost on restart — any users or clients created at
+runtime through the web interface will not survive a restart. Optionally, a SQLite database file can be configured to
+persist data between restarts; see the [Database configuration](DevOidcToolkit.Documentation/docs/configuration.md)
+for details.
The frontend is styled using basic styling, using the [Sakura CSS library](https://github.com/oxalorg/sakura).
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3aa0dd0..931928c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+- Add optional SQLite persistence via `Database.SqliteFile` config option; defaults to in-memory when not set (see [#20](https://github.com/BusinessSimulations/dev-oidc-toolkit/issues/20))
## [0.5.0]
- Add configurable user roles through `DevOidcToolkit__Users__INDEX__Roles__INDEX` (see [#17](https://github.com/BusinessSimulations/dev-oidc-toolkit/pull/17))
- Add runtime user registration at `/users` page (see [#15](https://github.com/BusinessSimulations/dev-oidc-toolkit/issues/15))
diff --git a/DevOidcToolkit.Documentation/docs/configuration.md b/DevOidcToolkit.Documentation/docs/configuration.md
index d996995..cb06473 100644
--- a/DevOidcToolkit.Documentation/docs/configuration.md
+++ b/DevOidcToolkit.Documentation/docs/configuration.md
@@ -59,6 +59,12 @@ This is a list of all of the environment variables that can be used to configure
https://fake-issuer.example.com |
None (derived from request URL) |
+
+ | DevOidcToolkit__Database__SqliteFile |
+ The path to the SQLite database file. When set, data is persisted to this file and survives restarts. When not set, an in-memory database is used and all data is lost on restart. |
+ /data/dev-oidc-toolkit.db |
+ None (in-memory) |
+
| DevOidcToolkit__Logging__MinimumLevel |
The minimum log level, possible values are Trace, Debug, Information, Warning, Error, Critical. |
@@ -204,6 +210,13 @@ details](#example-json-configuration)).
https://fake-issuer.example.com |
None |
+
+ | Database |
+ object |
+ The database configuration, see Database for more information. |
+ See Database for more information. |
+ None (in-memory) |
+
| Https |
object |
@@ -352,6 +365,38 @@ details](#example-json-configuration)).
+#### Database
+
+The database configuration controls how data is stored. By default, an in-memory database is used and all data
+(including users and clients created at runtime) is lost when the application stops. Set `SqliteFile` to a file path to
+use a SQLite database instead, which persists data between restarts.
+
+!!! note "Limitations"
+ The SQLite database schema is created automatically on first run using `EnsureCreated`. There are no migrations
+ supported — if the schema changes in a future version of dev-oidc-toolkit you may need to delete and recreate the
+ database file.
+
+
+
+
+ | Property |
+ Type |
+ Description |
+ Example |
+ Default Value |
+
+
+
+
+ | SqliteFile |
+ string |
+ Path to the SQLite database file. When set, all data is persisted to this file. When omitted, an in-memory database is used and data is lost on restart. |
+ /data/dev-oidc-toolkit.db |
+ None (in-memory) |
+
+
+
+
#### Users
@@ -435,6 +480,8 @@ details](#example-json-configuration)).
### Example JSON configuration
+In-memory database (default, no persistence):
+
```json
{
"DevOidcToolkit": {
@@ -459,3 +506,32 @@ details](#example-json-configuration)).
}
}
```
+
+SQLite database (data persists across restarts):
+
+```json
+{
+ "DevOidcToolkit": {
+ "Port": 8080,
+ "Database": {
+ "SqliteFile": "/data/dev-oidc-toolkit.db"
+ },
+ "Users": [
+ {
+ "Email": "sudo@localhost",
+ "FirstName": "Test",
+ "LastName": "User"
+ }
+ ],
+ "Clients": [
+ {
+ "Id": "test",
+ "Secret": "ThisIsNotARealSecret",
+ "RedirectUris": [
+ "http://localhost:3000/callback"
+ ]
+ }
+ ]
+ }
+}
+```
diff --git a/DevOidcToolkit.Documentation/docs/runtime-management.md b/DevOidcToolkit.Documentation/docs/runtime-management.md
index 0a297f7..d08ed2a 100644
--- a/DevOidcToolkit.Documentation/docs/runtime-management.md
+++ b/DevOidcToolkit.Documentation/docs/runtime-management.md
@@ -56,6 +56,6 @@ URIs are validated on submission to ensure they are properly formatted.
## Important Notes
-- **No Persistence**: Users and clients created at runtime exist only in the in-memory database. They will be lost when the application restarts.
+- **Persistence**: By default, users and clients created at runtime exist only in the in-memory database and will be lost when the application restarts. To persist runtime changes, configure a [SQLite database](configuration.md#database).
- **Configuration + Runtime**: You can use both configuration-based users/clients and runtime-created ones simultaneously.
- **Role Management**: Roles created at runtime persist for the lifetime of the application and can be assigned to multiple users.
diff --git a/DevOidcToolkit/DevOidcToolkit.csproj b/DevOidcToolkit/DevOidcToolkit.csproj
index 8fe40f1..64d5b58 100644
--- a/DevOidcToolkit/DevOidcToolkit.csproj
+++ b/DevOidcToolkit/DevOidcToolkit.csproj
@@ -19,6 +19,7 @@
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/DevOidcToolkit/Infrastructure/Configuration/Configuration.cs b/DevOidcToolkit/Infrastructure/Configuration/Configuration.cs
index 3198f64..83a58e9 100644
--- a/DevOidcToolkit/Infrastructure/Configuration/Configuration.cs
+++ b/DevOidcToolkit/Infrastructure/Configuration/Configuration.cs
@@ -18,6 +18,7 @@ public class DevOidcToolkitConfiguration
[ValidateObjectMembers] public HttpsConfiguration? Https { get; set; }
[ValidateObjectMembers] public LoggingConfiguration Logging { get; set; } = new LoggingConfiguration();
+ [ValidateObjectMembers] public DatabaseConfiguration Database { get; set; } = new DatabaseConfiguration();
}
public class UserConfiguration
@@ -89,4 +90,9 @@ public class LoggingConfiguration
{
public LogEventLevel MinimumLevel { get; set; } = LogEventLevel.Information;
public bool UseXForwardedForHeader { get; set; } = false;
+}
+
+public class DatabaseConfiguration
+{
+ public string? SqliteFile { get; set; }
}
\ No newline at end of file
diff --git a/DevOidcToolkit/Program.cs b/DevOidcToolkit/Program.cs
index 196528a..bfd46b9 100644
--- a/DevOidcToolkit/Program.cs
+++ b/DevOidcToolkit/Program.cs
@@ -5,6 +5,7 @@
using DevOidcToolkit.Infrastructure.Database;
using Microsoft.AspNetCore.Identity;
+using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
@@ -30,7 +31,18 @@
builder.Services.AddDbContext(options =>
{
- options.UseInMemoryDatabase("dev-auth");
+ if (config.Database.SqliteFile is not null)
+ {
+ var connectionString = new SqliteConnectionStringBuilder
+ {
+ DataSource = config.Database.SqliteFile
+ }.ToString();
+ options.UseSqlite(connectionString);
+ }
+ else
+ {
+ options.UseInMemoryDatabase("dev-auth");
+ }
options.UseOpenIddict();
});
@@ -179,12 +191,23 @@
var services = scope.ServiceProvider;
var db = services.GetRequiredService();
+ if (config.Database.SqliteFile is not null)
+ {
+ db.Database.EnsureCreated();
+ }
+
// Set up users and clients in the DB
var userManager = services.GetRequiredService>();
var roleManager = services.GetRequiredService>();
for (var i = 0; i < config.Users.Count; i++)
{
var user = config.Users[i];
+
+ if (await userManager.FindByEmailAsync(user.Email) is not null)
+ {
+ continue;
+ }
+
var userEntity = new DevOidcToolkitUser()
{
Id = i.ToString(),
@@ -215,6 +238,11 @@
var openIddictManager = scope.ServiceProvider.GetRequiredService();
foreach (var client in config.Clients)
{
+ if (await openIddictManager.FindByClientIdAsync(client.Id) is not null)
+ {
+ continue;
+ }
+
var clientApp = new OpenIddictApplicationDescriptor()
{
ClientId = client.Id,