Skip to content

Commit

Permalink
add refreshtokenflow_en.txt | add authenticationflow_en.txt | add cod…
Browse files Browse the repository at this point in the history
…e snippets
  • Loading branch information
ketteiteki committed Jun 22, 2023
1 parent 1617ae1 commit d8dcc40
Show file tree
Hide file tree
Showing 15 changed files with 958 additions and 3 deletions.
10 changes: 10 additions & 0 deletions src/code_snippets/02_openidconnect_add_scope_openid.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
\begin{spverbatim}
serviceCollection
.AddAuthentication(options => {...})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => {...})
.AddOpenIdConnect(AuthConstants.AppOidc, options =>
{
...
options.Scope.Add("openid");
});
\end{spverbatim}
11 changes: 11 additions & 0 deletions src/code_snippets/03_user_session_entity.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
\begin{spverbatim}
public class UserSessionEntity
{
public Guid Id { get; private set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset ExpiresAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public DateTimeOffset DateOfLastAccess { get; set; }
public byte[] Value { get; set; }
}
\end{spverbatim}
222 changes: 222 additions & 0 deletions src/code_snippets/04_implementation_ticket_store.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
\begin{spverbatim}
public class TicketStore : ITicketStore
{
private readonly DatabaseContext _context;
private readonly TicketSerializer _ticketSerializer;
private readonly HttpClient _httpClient;
private readonly AzureAdConfiguration _azureAdConfiguration;
private readonly IMemoryCache _memoryCache;
private readonly MemoryCacheEntryOptions _memoryCacheEntryOptions;

public TicketStore(
DatabaseContext context,
TicketSerializer ticketSerializer,
HttpClient httpClient,
AzureAdConfiguration azureAdConfiguration,
IMemoryCache memoryCache)
{
_context = context;
_ticketSerializer = ticketSerializer;
_httpClient = httpClient;
_azureAdConfiguration = azureAdConfiguration;
_memoryCache = memoryCache;

_memoryCacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(15))
.SetAbsoluteExpiration(TimeSpan.FromSeconds(100));
}

public async Task<string> StoreAsync(AuthenticationTicket ticket)
{
var idToken = ticket.Properties.GetTokenValue(TokenTypes.IdentityToken);
var accessToken = ticket.Properties.GetTokenValue(TokenTypes.AccessToken);
var refreshToken = ticket.Properties.GetTokenValue(TokenTypes.RefreshToken);

var handler = new JwtSecurityTokenHandler();
var decodeToken = handler.ReadToken(idToken) as JwtSecurityToken;
var ticketExpiresUtc = ticket.Properties.ExpiresUtc;

if (decodeToken == null)
{
throw new StoreException("Read token error");
}

if (ticketExpiresUtc.HasValue == false)
{
throw new StoreException("Ticket ExpiresUtc value does not exist");
}

var sessionId = decodeToken.Claims.First(x => x.Type == ClaimsConstants.Sid).Value;
var userSession = await _context.UserSessions.FirstOrDefaultAsync(x => x.Id == new Guid(sessionId));

if (userSession != null)
{
var deserializedTicket = _ticketSerializer.Deserialize(userSession.Value);

if (deserializedTicket == null)
{
throw new StoreException("Deserialization ticket error");
}

if (accessToken == null || refreshToken == null || idToken == null)
{
throw new StoreException("Access token, refresh token, identity token are not existing");
}

deserializedTicket.Properties.UpdateTokenValue(TokenTypes.AccessToken, accessToken);
deserializedTicket.Properties.UpdateTokenValue(TokenTypes.RefreshToken, refreshToken);
deserializedTicket.Properties.UpdateTokenValue(TokenTypes.IdentityToken, idToken);

var serializedTicket = _ticketSerializer.Serialize(deserializedTicket);

userSession.UpdateValue(serializedTicket);
userSession.UpdateExpiresAt(ticketExpiresUtc.Value);

_context.UserSessions.Update(userSession);
await _context.SaveChangesAsync();

_memoryCache.Set(sessionId, ticket, _memoryCacheEntryOptions);
}

if (userSession == null)
{
var serializedTicket = _ticketSerializer.Serialize(ticket);

var newUserSession = new UserSessionEntity(new Guid(sessionId), ticketExpiresUtc.Value, serializedTicket);

_context.UserSessions.Add(newUserSession);
await _context.SaveChangesAsync();

_memoryCache.Set(sessionId, ticket, _memoryCacheEntryOptions);
}

return sessionId;
}

public async Task RenewAsync(string key, AuthenticationTicket authenticationTicket)
{
var userSession = await _context.UserSessions.FirstOrDefaultAsync(x => x.Id == new Guid(key));

if (userSession == null)
{
throw new StoreException("UserSession does not exist");
}

var deserializedTicket = _ticketSerializer.Deserialize(userSession.Value);

if (deserializedTicket == null)
{
throw new StoreException("Deserialization ticket error");
}

var refreshToken = deserializedTicket.Properties.GetTokenValue(TokenTypes.RefreshToken);

var response = await RequestRefreshTokenAsync(refreshToken);

if (response.AccessToken == null || response.RefreshToken == null || response.IdentityToken == null)
{
await RemoveAsync(key);
return;
}

deserializedTicket.Properties.UpdateTokenValue(TokenTypes.AccessToken, response.AccessToken);
deserializedTicket.Properties.UpdateTokenValue(TokenTypes.RefreshToken, response.RefreshToken);
deserializedTicket.Properties.UpdateTokenValue(TokenTypes.IdentityToken, response.IdentityToken);

var serializedTicket = _ticketSerializer.Serialize(deserializedTicket);

userSession.UpdateValue(serializedTicket);
userSession.UpdateExpiresAt(userSession.ExpiresAt.AddSeconds(response.ExpiresIn));

_context.UserSessions.Update(userSession);
await _context.SaveChangesAsync();
}

public async Task<AuthenticationTicket> RetrieveAsync(string key)
{
var isMemoryCacheTicketExist = _memoryCache.TryGetValue(key, out AuthenticationTicket memoryCacheTicket);

if (isMemoryCacheTicketExist)
{
return memoryCacheTicket;
}

var userSession = await _context.UserSessions.AsNoTracking().FirstOrDefaultAsync(x => x.Id == new Guid(key));

if (userSession == null)
{
return null;
}

var deserializedTicket = _ticketSerializer.Deserialize(userSession.Value);

if (deserializedTicket == null)
{
throw new StoreException("Deserialization ticket error");
}

if (DateTimeOffset.UtcNow > userSession.ExpiresAt)
{
var refreshToken = deserializedTicket.Properties.GetTokenValue(TokenTypes.RefreshToken);

if (refreshToken == null)
{
throw new StoreException("Refresh token does not exist");
}

var response = await RequestRefreshTokenAsync(refreshToken);

if (response.AccessToken == null || response.RefreshToken == null || response.IdentityToken == null)
{
await RemoveAsync(key);
return null;
}

deserializedTicket.Properties.UpdateTokenValue(TokenTypes.AccessToken, response.AccessToken);
deserializedTicket.Properties.UpdateTokenValue(TokenTypes.RefreshToken, response.RefreshToken);
deserializedTicket.Properties.UpdateTokenValue(TokenTypes.IdentityToken, response.IdentityToken);

var serializedTicket = _ticketSerializer.Serialize(deserializedTicket);

userSession.UpdateValue(serializedTicket);
userSession.UpdateExpiresAt(DateTimeOffset.UtcNow.AddSeconds(response.ExpiresIn));
}

userSession.UpdateDateOfLastAccess();
_context.UserSessions.Update(userSession);

await _context.SaveChangesAsync();

_memoryCache.Set(key, deserializedTicket, _memoryCacheEntryOptions);

return deserializedTicket;
}

public async Task RemoveAsync(string key)
{
var userSession = await _context.UserSessions.FirstAsync(x => x.Id == new Guid(key));

if (userSession == null) return;

_context.UserSessions.Remove(userSession);
await _context.SaveChangesAsync();
}

private async Task<TokenResponse> RequestRefreshTokenAsync(string refreshToken)
{
var refreshTokenRequest = new RefreshTokenRequest
{
Address = _azureAdConfiguration.AzureAdTokenUrl,
ClientId = _azureAdConfiguration.ClientId.ToString(),
ClientSecret = _azureAdConfiguration.ClientSecret,
GrantType = GrantType.RefreshToken,
Scope = _azureAdConfiguration.Scopes,
RefreshToken = refreshToken
};

var response = await _httpClient.RequestRefreshTokenAsync(refreshTokenRequest);

return response;
}
}
\end{spverbatim}
29 changes: 29 additions & 0 deletions src/code_snippets/05_configuration_ticker_store.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
\begin{spverbatim}
public static class TicketStoreDependencyInjection
{
public static IServiceCollection AddTicketStore(this IServiceCollection serviceCollection)
{
var serviceProvider = serviceCollection.BuildServiceProvider();

var configuration = serviceProvider.GetService<IConfiguration>();
var dbContext = serviceProvider.GetService<DatabaseContext>();
var ticketSerializer = new TicketSerializer();
var httpClient = new HttpClient();
var memoryCache = serviceProvider.GetService<IMemoryCache>();

var azAdSection = configuration.GetSection(AppSettingsConstants.AzureAdSelection);
var azureAdConfiguration = azAdSection.Get<AzureAdConfiguration>();
var adClientSecret = Environment.GetEnvironmentVariable(AppSettingsConstants.AdSecretKey);
azureAdConfiguration.ClientSecret = adClientSecret;

var ticketStore = new TicketStore(dbContext, ticketSerializer, httpClient, azureAdConfiguration, memoryCache);

serviceCollection.AddSingleton<ITicketStore, TicketStore>(_ => ticketStore);
serviceCollection
.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
.Configure<ITicketStore>((o, _) => o.SessionStore = ticketStore);

return serviceCollection;
}
}
\end{spverbatim}
3 changes: 3 additions & 0 deletions src/code_snippets/06_registration_ticker_store.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
\begin{spverbatim}
builder.Services.AddTicketStore();
\end{spverbatim}
54 changes: 54 additions & 0 deletions src/code_snippets/07_implementation_background_service.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
\begin{spverbatim}
public class RefreshBackgroundService : IHostedService
{
private readonly DatabaseContext _context;
private readonly TicketStore _ticketStore;

public RefreshBackgroundService(DatabaseContext context, TicketStore ticketStore)
{
_context = context;
_ticketStore = ticketStore;
}

public Task StartAsync(CancellationToken cancellationToken)
{
var _ = StartRefreshingUserSessionsAsync(cancellationToken);

return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}

private async Task StartRefreshingUserSessionsAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var expiringUserSessions = await _context.UserSessions
.Where(x => (x.ExpiresAt > DateTimeOffset.UtcNow &&
x.ExpiresAt < DateTimeOffset.UtcNow.AddMinutes(5)) ||
x.ExpiresAt < DateTimeOffset.UtcNow)
.ToListAsync(cancellationToken);

foreach (var userSession in expiringUserSessions)
{
var differenceBetweenLastAccessAndUtcNow = userSession.DateOfLastAccess
.Subtract(DateTimeOffset.UtcNow)
.Duration();

if (differenceBetweenLastAccessAndUtcNow > TimeSpan.FromDays(3))
{
await _ticketStore.RemoveAsync(userSession.Id.ToString());
continue;
}

await _ticketStore.RenewAsync(userSession.Id.ToString(), null);
}

await Task.Delay(TimeSpan.FromMinutes(10), cancellationToken);
}
}
}
\end{spverbatim}
34 changes: 34 additions & 0 deletions src/code_snippets/08_configuration_background_service.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
\begin{spverbatim}
public static class HostedServicesDependencyInjection
{
public static IServiceCollection AddHostedServices(this IServiceCollection serviceCollection)
{
var serviceProvider = serviceCollection.BuildServiceProvider();

var configuration = serviceProvider.GetService<IConfiguration>();
var databaseContext = serviceProvider.GetService<DatabaseContext>();
var memoryCache = serviceProvider.GetService<IMemoryCache>();

var tickerSerializer = new TicketSerializer();
var httpClient = new HttpClient();

var azAdSection = configuration.GetSection(AppSettingsConstants.AzureAdSelection);
var azureAdConfiguration = azAdSection.Get<AzureAdConfiguration>();
var adClientSecret = Environment.GetEnvironmentVariable(AppSettingsConstants.AdSecretKey);
azureAdConfiguration.ClientSecret = adClientSecret;

var tickerStore = new TicketStore(
databaseContext,
tickerSerializer,
httpClient,
azureAdConfiguration,
memoryCache);

var refreshBackgroundService = new RefreshBackgroundService(databaseContext, tickerStore);

serviceCollection.AddHostedService(_ => refreshBackgroundService);

return serviceCollection;
}
}
\end{spverbatim}
3 changes: 3 additions & 0 deletions src/code_snippets/09_registration_background_service.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
\begin{spverbatim}
builder.Services.AddHostedServices();
\end{spverbatim}
Loading

0 comments on commit d8dcc40

Please sign in to comment.