Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #16 Implement o auth Google #61

Merged
merged 8 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .env

This file was deleted.

47 changes: 47 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# PostgreSQL

# The name of the database user
POSTGRES_USER=
# The password for the database user
POSTGRES_PASSWORD=

# Keycloak

# The username of the keycloak admin user
KEYCLOAK_ADMIN=
# The password of the keycloak admin user
KEYCLOAK_ADMIN_PASSWORD=
# If enable the health-check endpoints in the keycloak
KC_HEALTH_ENABLED=true
# The name of the database provider for the keycloak (we are using our PostgreSQL database from the container)
KC_DB=postgres
# The URL connection to the keycloak database
# E.g: jdbc:postgresql://<database_container>:<database_port>/keycloak
KC_DB_URL=
# The name of the user that keycloak will use to connect into the database
KC_DB_USERNAME=
# The password of the user that keycloak will use to connect into the database
KC_DB_PASSWORD=

# Keycloak app (realm-export.json)

# The name of the realm in the keycloak that will be used by application
KSUMMARIZED_REALM_NAME=KnowledgeSummarized
# The secret for the `ksummarized` client that is used by frontend application during authentication
KSUMMARIZED_CLIENT_SECRET=
# The ClientId for the Google OAuth provider
PROVIDER_GOOGLE_ID=
# The ClientSecret for the Google OAuth provider
PROVIDER_GOOGLE_SECRET=
# The ClientId for the Twitter/X OAuth provider
PROVIDER_TWITTER_X_ID=
# The ClientSecret for the Twitter/X OAuth provider
PROVIDER_TWITTER_X_SECRET=
# The ClientId for the GitHub OAuth provider
PROVIDER_GITHUB_ID=
# The ClientSecret for the GitHub OAuth provider
PROVIDER_GITHUB_SECRET=
# The ClientId for the Facebook OAuth provider
PROVIDER_FACEBOOK_ID=
# The ClientSecret for the Facebook OAuth provider
PROVIDER_FACEBOOK_SECRET=
4 changes: 2 additions & 2 deletions .github/workflows/sonarcloud-backend-build-and-analyze.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
name: Build and analyze
runs-on: windows-latest
steps:
- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: 11
java-version: 17
distribution: "zulu" # Alternative distribution options are available.
- uses: actions/checkout@v3
with:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,7 @@ node_modules
dist
dist-ssr
*.local

# Environment files
.env
appsettings.Development.json
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ To run this application locally it is recommended to have the following installe
- dotnet sdk
- entity framework tools

Firstly there is a need to configure environment variables:

1. Copy `.env.example` as `.env` and populate the environment variables.
1. Copy `appsettings.json` as `appsettings.Development.json` and populate the variables.

Next install dev-certs to use https in powershell

```powershell
dotnet dev-certs https -ep ".aspnet\https\aspnetapp.pfx" -p devcertpasswd --trust
```

or in bash/zsh

```bash
dotnet dev-certs https -ep .aspnet/https/aspnetapp.pfx -p devcertpasswd --trust
```

Next go to the `scripts` directory and run `apply_migrations.ps1`
Next You should go back to the main directory and run `docker compose up --build`
This can be done with the following snippet.
Expand Down
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/aspnet:7.0.2-alpine3.17-amd64 AS base
FROM mcr.microsoft.com/dotnet/aspnet:7.0.10-alpine3.18 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
Expand Down
6 changes: 6 additions & 0 deletions backend/src/api/Constants/ErrorMessages.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace api.Constants;

public static class ErrorMessages
{
public const string UserAlreadyExists = "User already exists";
}
92 changes: 35 additions & 57 deletions backend/src/api/Controllers/AuthenticationController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using api.Data.DTO;
using api.Constants;
using api.Data.DTO;
using api.Services.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.IdentityModel.Tokens.Jwt;

namespace api.Controllers;

Expand All @@ -10,77 +12,53 @@ namespace api.Controllers;
public class AuthenticationController : ControllerBase
{
private readonly IUserService _userService;
private readonly ILogger<AuthenticationController> _logger;

public AuthenticationController(IUserService userService)
public AuthenticationController(IUserService userService, ILogger<AuthenticationController> logger)
{
_userService = userService;
_logger = logger;
}

[HttpPost("register")]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
public async Task<IActionResult> Register([FromBody] UserDTO user)
[HttpGet("create-user")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateUser()
{
if (!ModelState.IsValid) { return BadRequest("Invalid data provided!"); }
var accessToken = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
var jsonTokenData = new JwtSecurityTokenHandler().ReadJwtToken(accessToken);
var keycloakUuid = jsonTokenData.Subject;
var userEmail = jsonTokenData.Claims.FirstOrDefault(claim => claim.Type == "email")?.Value;

var (IsSuccess, Error) = await _userService.Register(user);
if (IsSuccess)
if (userEmail == null)
{
return Ok("User created");
}
else
{
return BadRequest(Error);
_logger.LogInformation("User with keycloakUuid={keycloakUuid} tried to log in without email.", keycloakUuid);
return BadRequest("Required data missing");
}
}

[HttpPost("login")]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
public async Task<IActionResult> Login([FromBody] UserDTO user)
{
if (!ModelState.IsValid) { return BadRequest("Please provide login credentials!"); }
var (isSuccess, error) = await _userService.CreateKeycloakUser(
new UserDto
{
KeycloakUuid = keycloakUuid,
Email = userEmail
}
);

var (IsSuccess, AuthResult, Error) = await _userService.Login(user);
if (IsSuccess)
if (isSuccess)
{
return Ok(AuthResult);
}
else
{
return Unauthorized(Error);
}
}

[HttpPost("refresh-token")]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
public async Task<IActionResult> RefreshToken([FromBody] TokenRequestDTO tokenRequestDTO)
{
if (!ModelState.IsValid) { return BadRequest("Invalid token request."); }
var (IsSuccess, AuthResult, Error) = await _userService.RefreshLogin(tokenRequestDTO);
if (IsSuccess)
{
return Ok(AuthResult);
_logger.LogInformation("The user {email} has been created successfully.", userEmail);
return Ok("User created");
}
else
{
return Unauthorized(Error);
}
}

[HttpPost("logout")]
[ProducesResponseType(200)]
[Authorize()]
public async Task<IActionResult> Logout()
{
if (HttpContext.User?.Identity?.Name is not null)
{
await _userService.Logout(HttpContext.User.Identity.Name);
if (error == ErrorMessages.UserAlreadyExists)
{
_logger.LogInformation("The user {email} has already been created.", userEmail);
return Ok("User is already created");
}
_logger.LogInformation("Failure during creation of {email} user.", userEmail);
return BadRequest(error);
}
return Ok("The user has been logged out.");
}

}
4 changes: 2 additions & 2 deletions backend/src/api/Controllers/GreetingsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ public class GreetingsController : ControllerBase
{
[HttpGet]
[Authorize]
[ProducesResponseType(200)]
[ProducesResponseType(StatusCodes.Status200OK)]
[Route("user")]
public IActionResult Greet()
{
return Ok($"Hello {HttpContext?.User?.Identity?.Name ?? "World" }");
}

[HttpGet]
[ProducesResponseType(200)]
[ProducesResponseType(StatusCodes.Status200OK)]
[Route("HelloWorld")]
public IActionResult HelloWorld()
{
Expand Down
19 changes: 0 additions & 19 deletions backend/src/api/Data/DAO/RefreshToken.cs

This file was deleted.

9 changes: 7 additions & 2 deletions backend/src/api/Data/DAO/UserModel.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations;

namespace api.Data.DAO;

public class UserModel : IdentityUser
public class UserModel
{
[Key]
public int Id { get; set; }
public required string KeycloakUuid { get; set; }
[EmailAddress]
public required string Email { get; set; }
}
10 changes: 0 additions & 10 deletions backend/src/api/Data/DTO/AuthResultDTO.cs

This file was deleted.

12 changes: 0 additions & 12 deletions backend/src/api/Data/DTO/TokenRequestDTO.cs

This file was deleted.

9 changes: 4 additions & 5 deletions backend/src/api/Data/DTO/UserDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

namespace api.Data.DTO;

public class UserDTO
public class UserDto
mtracewicz marked this conversation as resolved.
Show resolved Hide resolved
{
[Required]
[EmailAddress]
public string Email { get; set; }

public required string KeycloakUuid { get; set; }
[Required]
public string Password { get; set; }
[EmailAddress]
public required string Email { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
namespace api.Data;
public class JwtOptions
public class KeycloakJwtOptions
{
public string? Issuer { get; set; }
mtracewicz marked this conversation as resolved.
Show resolved Hide resolved
public string? Audience { get; set; }
Expand Down
4 changes: 2 additions & 2 deletions backend/src/api/Data/UsersDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

namespace api.Data;

public class UsersDbContext : IdentityDbContext<UserModel>
public class UsersDbContext : DbContext
mtracewicz marked this conversation as resolved.
Show resolved Hide resolved
{
public UsersDbContext(DbContextOptions<UsersDbContext> options) : base(options)
{
}
public DbSet<RefreshToken> RefreshTokens { get; set; }
public DbSet<UserModel> Users { get; set; }
}
45 changes: 45 additions & 0 deletions backend/src/api/LoggingExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Serilog;
using Serilog.Configuration;
using Serilog.Core;
using Serilog.Events;

namespace api;

public static class LoggingExtensions
{
public static LoggerConfiguration WithCorrelationId(this LoggerEnrichmentConfiguration config)
=> config.With(new CorrelationIdEnricher());
}

public class CorrelationIdEnricher : ILogEventEnricher
{
private const string _propertyName = "CorelationId";
private readonly IHttpContextAccessor _contextAccessor;

public CorrelationIdEnricher()
{
_contextAccessor = new HttpContextAccessor();
}

public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var httpContext = _contextAccessor.HttpContext;
if (httpContext == null)
{
return;
}

if (httpContext.Items[_propertyName] is LogEventProperty logEventProperty)
{
logEvent.AddPropertyIfAbsent(logEventProperty);
return;
}

var correlationId = Guid.NewGuid().ToString();

var correlationIdProperty = new LogEventProperty(_propertyName, new ScalarValue(correlationId));
logEvent.AddOrUpdateProperty(correlationIdProperty);

httpContext.Items.Add(_propertyName, correlationIdProperty);
}
}
Loading
Loading