WIP: backend protection

This commit is contained in:
2022-12-06 12:27:09 +01:00
parent 7330fb90f2
commit 55b5150049
23 changed files with 499 additions and 114 deletions

View File

@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33110.190
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAPI", "WebAPI\WebAPI.csproj", "{EB898292-5370-45C7-85B7-FE24D110A8C6}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAPI", "WebAPI\WebAPI.csproj", "{799D68C2-A4C1-43F8-8C35-1126C0AC32D6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,15 +11,15 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EB898292-5370-45C7-85B7-FE24D110A8C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EB898292-5370-45C7-85B7-FE24D110A8C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB898292-5370-45C7-85B7-FE24D110A8C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB898292-5370-45C7-85B7-FE24D110A8C6}.Release|Any CPU.Build.0 = Release|Any CPU
{799D68C2-A4C1-43F8-8C35-1126C0AC32D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{799D68C2-A4C1-43F8-8C35-1126C0AC32D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{799D68C2-A4C1-43F8-8C35-1126C0AC32D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{799D68C2-A4C1-43F8-8C35-1126C0AC32D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C1F5C21F-B331-4C5D-BDFF-3FAC8116996F}
SolutionGuid = {2CFA03A7-56D9-4ADE-9B6A-1A3383A1C104}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using WebAPI.Models;
namespace WebAPI
{
public class AppDbContext : DbContext
{
public DbSet<User> Users { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
}
public static readonly Microsoft.Extensions.Logging.LoggerFactory _myLoggerFactory =
new LoggerFactory(new[] {
new Microsoft.Extensions.Logging.Debug.DebugLoggerProvider()
});
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLoggerFactory(_myLoggerFactory);
}
}
}

View File

@@ -0,0 +1,72 @@
using Google.Apis.Auth;
using Google.Apis.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Client.Platforms.Features.DesktopOs.Kerberos;
using Microsoft.IdentityModel.Tokens;
using System.Configuration;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using WebAPI.Models;
namespace WebAPI.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class AuthController : Controller
{
private readonly AppDbContext db;
private readonly IConfiguration configuration;
public AuthController(
AppDbContext _db, IConfiguration _configuration)
{ db = _db; configuration = _configuration; }
[HttpPost]
[Route("apiToken")]
public async Task<IActionResult> apiToken([FromBody] string credential)
{
var settings = new GoogleJsonWebSignature.ValidationSettings()
{
Audience = new List<string> { configuration.GetValue<string>("GoogleClientId") }
};
var payload = await GoogleJsonWebSignature.ValidateAsync(credential, settings);
var user = db.Users.Where(x => x.Email == payload.Email).FirstOrDefault();
if (user != null)
{
return Ok(JWTGenerator(user));
}
else
{
return BadRequest();
}
}
private dynamic JWTGenerator(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(configuration.GetValue<string>("Secret"));
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[] { new Claim("username", user.UserName) }),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var encrypterToken = tokenHandler.WriteToken(token);
HttpContext.Response.Cookies.Append("token", encrypterToken,
new CookieOptions
{
Expires = DateTime.Now.AddDays(7),
HttpOnly = true,
Secure = true,
IsEssential = true,
SameSite = SameSiteMode.None
});
return new { token = encrypterToken, username = user.UserName };
}
}
}

View File

@@ -0,0 +1,28 @@
using Google.Apis.Auth;
using Google.Apis.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Client.Platforms.Features.DesktopOs.Kerberos;
using Microsoft.IdentityModel.Tokens;
using System.Configuration;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using WebAPI.Models;
namespace WebAPI.Controllers
{
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class PingController : Controller
{
[HttpGet]
[Route("Ping")]
public IActionResult Ping()
{
return Ok(new { data = "Pong" });
}
}
}

View File

@@ -0,0 +1,51 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using WebAPI;
#nullable disable
namespace WebAPI.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20221205190148_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("WebAPI.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Email")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserName")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace WebAPI.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Email = table.Column<string>(type: "nvarchar(max)", nullable: true),
UserName = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@@ -0,0 +1,48 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using WebAPI;
#nullable disable
namespace WebAPI.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("WebAPI.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Email")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserName")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace WebAPI.Models
{
public class User
{
#region Properties
[Key]
public Guid Id { get; set; }
public string? Email { get; set; }
[StringLength(50)]
public string? UserName { get; set; }
public DateTime CreatedAt { get; set; }
#endregion
}
}

81
Diuna/WebAPI/Program.cs Normal file
View File

@@ -0,0 +1,81 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using WebAPI;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("SQLDatabase");
builder.Services.AddDbContext<AppDbContext>(x => x.UseSqlServer(connectionString));
builder.Services.AddCors(options =>
{
options.AddPolicy("CORSPolicy", builder =>
{
builder.WithOrigins("http://localhost:4200")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
builder.WithOrigins("https://diuna.bim-it.pl")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
builder.Services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddCookie(x =>
{
x.Cookie.Name = "token";
}).AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration.GetValue<string>("Secret"))),
ValidateIssuer = false,
ValidateAudience = false
};
x.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
//context.Token = context.Request.Cookies["token"];
context.Token = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ik1pY2hhbCBaaWVsaW5za2kiLCJuYmYiOjE2NzAyODk3NTAsImV4cCI6MTY3MDg5NDU1MCwiaWF0IjoxNjcwMjg5NzUwfQ.XZ1lE_Jio9N5aetvY8qX8rS2xoIcPw3GJWGSatPh1VokQkrILOowvvibdGViQOOi39qGBOFKa8JC61XcaL-1qw";
return Task.CompletedTask;
}
};
});
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// app.UseHttpsRedirection();
app.UseCors("CORSPolicy");
app.UseAuthorization();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@@ -4,8 +4,8 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:1860",
"sslPort": 44315
"applicationUrl": "http://localhost:12241",
"sslPort": 44358
}
},
"profiles": {
@@ -13,8 +13,8 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "https://localhost:7170;http://localhost:5008",
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7009;http://localhost:5183",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -22,7 +22,7 @@
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Apis.Auth" Version="1.58.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="7.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SQLDatabase": "Server=tcp:localhost,1433;Initial Catalog=diuna;Persist Security Info=False;User ID=SA;Password=v](8Lc|RfG;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=False;Connection Timeout=30;"
},
"GoogleCLientId": "107631825312-bkfe438ehr9k9ecb2h76g802tj6advma.apps.googleusercontent.com",
"Secret": "8393AF8EAEF8478CB738D44858690F9C7E2D19F65896DD9FBAA3EB2A6F493E80"
}

View File

@@ -7,6 +7,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MainViewComponent } from './main-view/main-view.component';
import { MaterialModule } from './material.module';
import { LoginPageComponent } from './components/login-page/login-page.component';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
@@ -20,6 +21,7 @@ import { LoginPageComponent } from './components/login-page/login-page.component
AppRoutingModule,
BrowserAnimationsModule,
MaterialModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]

View File

@@ -1,9 +1,60 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { User } from '../models/user';
@Injectable({
providedIn: 'root'
})
export class AuthServiceService {
export class AuthService {
constructor() { }
apiToken: string | null = null;
user: User | null = null;
constructor(
private http$: HttpClient
) { }
loadDbUser() {
return new Promise((resolve, reject) => {
const headers = new HttpHeaders({
// 'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiToken}`
})
this.http$.get<any>(`${environment.api.url}/ping/ping`, {
headers,
withCredentials: true
}).subscribe({
next: (data) => {
console.log('Ping', data);
resolve(data);
},
error: (e) => {
console.error('Ping error', e);
reject(e);
}
}
);
});
}
getAPIToken(credentials: string): Promise<void> {
return new Promise((resolve, reject) => {
const header = new HttpHeaders().set('Content-type', 'application/json');
this.http$.post<any>(`${environment.api.url}/auth/apiToken`, JSON.stringify(credentials), { headers: header }).subscribe({
next: (data) => {
console.log('apiToken', data);
this.apiToken = data.token;
resolve(data);
},
error: (e) => {
console.error('apiToken error', e);
reject(e);
}
}
);
});
//const header = new HttpHeaders().set('Content-type', 'application/json');
//return this.httpClient.post(this.path + "LoginWithGoogle", JSON.stringify(credentials), { headers: header });
}
}

View File

@@ -1,8 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import jwt_decode from "jwt-decode";
import { AuthService } from 'src/app/auth/auth.service';
import { User } from 'src/app/models/user';
import { DataService } from 'src/app/services/data.service';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-login-page',
@@ -13,38 +15,46 @@ import { DataService } from 'src/app/services/data.service';
export class LoginPageComponent implements OnInit {
constructor(
private data$: DataService,
private router$: Router
private router$: Router,
private auth$: AuthService
) {}
ngOnInit(): void {
// @ts-ignore
google.accounts.id.initialize({
client_id: "107631825312-bkfe438ehr9k9ecb2h76g802tj6advma.apps.googleusercontent.com",
callback: this.handleCredentialResponse.bind(this),
auto_select: true,
cancel_on_tap_outside: true,
});
// @ts-ignore
google.accounts.id.renderButton(
// @ts-ignore
document.getElementById("google-button"),
{ theme: "outline", size: "large", width: "100%" }
);
// @ts-ignore
google.accounts.id.prompt();
window.onGoogleLibraryLoad = () => {
// @ts-ignore
google.accounts.id.initialize({
client_id: environment.google.clientId,
callback: this.handleCredentialResponse.bind(this),
auto_select: true,
cancel_on_tap_outside: true
});
// @ts-ignore
google.accounts.id.renderButton(
// @ts-ignore
document.getElementById("google-button"),
{ theme: "outline", size: "large", width: "100%" }
);
// @ts-ignore
google.accounts.id.prompt((notification: PromptMomentNotification) => {});
};
}
async handleCredentialResponse(response: any) {
try {
console.log("Google Response", response);
const responsePayload: any = jwt_decode(response.credential);
this.data$.currentUser = new User({
id: 1,
googledId: responsePayload.aud,
googleCredentials: response.credential,
userName: `${responsePayload.given_name} ${responsePayload.family_name}`,
email: responsePayload.email,
avatar: responsePayload.picture
});
// this.auth$.loadDbUser();
this.router$.navigate(['/app']);
console.log("USer", this.data$.currentUser);
await this.auth$.getAPIToken(response.credential);
this.auth$.loadDbUser();
} catch (e) {
console.error('Get user error', e);
}

View File

@@ -2,8 +2,8 @@ export class User {
id!: string;
email!: string;
userName!: string;
googleId!: string;
avatar?: string | ArrayBuffer;
googleCredentials!: string;
avatar?: string;
constructor(input: any) {
Object.assign(this, input)
}

View File

@@ -3,7 +3,13 @@
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
production: false,
api: {
url: "http://localhost:5183/api"
},
google: {
clientId: "107631825312-bkfe438ehr9k9ecb2h76g802tj6advma.apps.googleusercontent.com"
}
};
/*

View File

@@ -1,33 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace WebAPI.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}

View File

@@ -1,17 +0,0 @@
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@@ -1,13 +0,0 @@
namespace WebAPI
{
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
}

View File

@@ -1,9 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}