feat/authorization (#39)

move client

merge

add Gut authentication and authorization

merge

dummy

Co-authored-by: MaxchilKH <m.w.bohdanowicz@gmail.com>
This commit is contained in:
maxchil 2020-09-13 01:07:40 +02:00
parent 5884a444a9
commit 54dfcaa7e7
24 changed files with 375 additions and 138 deletions

View File

@ -7,8 +7,13 @@ services:
CONNECTIONSTRINGS__INTERNSHIPDATABASE: "Host=db.postgres;Port=5432;Database=postgres;Username=postgres;Password=szwoniu"
ASPNETCORE_ENVIRONMENT: Development
ASPNETCORE_URLS: http://+:80
SECURITYOPTIONS__SECRET: PDv7DrqznYL6nv7DrqzjnQYO9JxIsWdcjnQYL6nu0f
SECURITYOPTIONS__SECRET: iewaiwie3aig9wi3chieBai9eephai
SECURITYOPTIONS__EXPIRATION: 20
SECURITYOPTIONS__BASEURL: https://logowanie.pg.edu.pl
SECURITYOPTIONS__TOKENPATH: /oauth2.0/accessToken
SECURITYOPTIONS__PROFILEPATH: /oauth2.0/profile
SECURITYOPTIONS__CLIENTID: PraktykiClientId
SECURITYOPTIONS__REDIRECTURL: https://system-praktyk.stg.kadet.net/user/login/check/pg
depends_on:
- db.postgres
ports:

View File

@ -1,11 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OAuth" Version="2.2.0" />
</ItemGroup>
</Project>

View File

@ -1,20 +0,0 @@
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
namespace AspNet.Security.OAuth.MyGut
{
public static class AuthenticationBuilderExtension
{
public static AuthenticationBuilder AddMyGut(this AuthenticationBuilder builder) =>
builder.AddMyGut(MyGutAuthenticationDefaults.AuthenticationScheme, MyGutAuthenticationDefaults.DisplayName, options => { });
public static AuthenticationBuilder AddMyGut(
this AuthenticationBuilder builder,
string scheme,
string caption,
Action<MyGutAuthenticationOptions> configuration
) =>
builder.AddOAuth<MyGutAuthenticationOptions, MyGutAuthenticationHandler>(scheme, caption, configuration);
}
}

View File

@ -1,20 +0,0 @@
namespace AspNet.Security.OAuth.MyGut
{
public static class MyGutAuthenticationConstants
{
public static class Urls
{
}
public static class Claims
{
public const string AlbumNumber = "urn:mygut:albumnumber";
}
public static class UrlQueryParameterValues
{
public const string Consent = "consent";
public const string None = "none";
}
}
}

View File

@ -1,19 +0,0 @@
namespace AspNet.Security.OAuth.MyGut
{
public static class MyGutAuthenticationDefaults
{
public const string AuthenticationScheme = "MyGut";
public const string DisplayName = "MyGut";
public const string Issuer = "MyGut";
public const string CallbackPath = "/signin-mygut";
public const string AuthorizationEndpoint = "https://logowanie.pg.edu.pl/login";
public const string TokenEndpoint = "https://logowanie.pg.edu.pl/login";
public const string UserInformationEndpoint = "https://logowanie.pg.edu.pl/login";
}
}

View File

@ -1,19 +0,0 @@
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace AspNet.Security.OAuth.MyGut
{
public class MyGutAuthenticationHandler : OAuthHandler<MyGutAuthenticationOptions>
{
public MyGutAuthenticationHandler(
IOptionsMonitor<MyGutAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
}
}

View File

@ -1,16 +0,0 @@
using Microsoft.AspNetCore.Authentication.OAuth;
namespace AspNet.Security.OAuth.MyGut
{
public class MyGutAuthenticationOptions : OAuthOptions
{
public MyGutAuthenticationOptions()
{
ClaimsIssuer = MyGutAuthenticationDefaults.Issuer;
CallbackPath = MyGutAuthenticationDefaults.CallbackPath;
AuthorizationEndpoint = MyGutAuthenticationDefaults.AuthorizationEndpoint;
TokenEndpoint = MyGutAuthenticationDefaults.TokenEndpoint;
UserInformationEndpoint = MyGutAuthenticationDefaults.UserInformationEndpoint;
}
}
}

View File

@ -1,15 +1,18 @@
using System;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using InternshipSystem.Api.Options;
using InternshipSystem.Api.Security;
using InternshipSystem.Core;
using InternshipSystem.Repository;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Serilog;
namespace InternshipSystem.Api.Controllers
{
@ -19,12 +22,18 @@ namespace InternshipSystem.Api.Controllers
{
private readonly InternshipDbContext _context;
private readonly JwtTokenService _tokenService;
private readonly GutCasClient _loginClient;
private readonly SecurityOptions _securityOptions;
public AccessController(IOptions<SecurityOptions> options, InternshipDbContext context, JwtTokenService tokenService)
public AccessController(
IOptions<SecurityOptions> options,
InternshipDbContext context,
JwtTokenService tokenService,
GutCasClient loginClient)
{
_context = context;
_tokenService = tokenService;
_loginClient = loginClient;
_securityOptions = options.Value;
}
@ -32,16 +41,47 @@ namespace InternshipSystem.Api.Controllers
[HttpGet("login")]
public async Task<ActionResult> Authenticate(string code, CancellationToken cancellationToken)
{
var token = await _loginClient.GetCasTokenAsync(code, cancellationToken);
var casData = await _loginClient.GetProfileAsync(token, cancellationToken);
if (!long.TryParse(casData.PersonNumber, out var id))
{
return BadRequest();
}
var student = await _context.Students.FirstOrDefaultAsync(s => s.Id == id);
if (student == null)
{
student = CreateStudentWithCasData(casData);
await _context.Students.AddAsync(student, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "Jan"),
new Claim(ClaimTypes.Surname, "Kowalski"),
new Claim(InternshipClaims.PersonNumber, "1")
new Claim(ClaimTypes.Name, student.FirstName),
new Claim(ClaimTypes.Surname, student.LastName),
new Claim(InternshipClaims.PersonNumber, student.Id.ToString())
});
return Ok(_tokenService.generateToken(identity));
}
[HttpGet("/dev/login")]
public async Task<ActionResult> Authenticate(CancellationToken cancellationToken)
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "firstname"),
new Claim(ClaimTypes.Surname, "lastname"),
new Claim(InternshipClaims.PersonNumber, "1")
});
return Ok(_tokenService.generateToken(identity));
}
[HttpGet("loginEdition")]
[Authorize]
public async Task<ActionResult> LoginIntoEdition(Guid editionId, [FromServices] User user, CancellationToken token)
@ -64,5 +104,16 @@ namespace InternshipSystem.Api.Controllers
return Ok(_tokenService.generateToken(newIdentity));
}
private Student CreateStudentWithCasData(CasUserData casData)
{
var id = long.Parse(casData.PersonNumber);
var firstName = casData.FirstName;
var lastName = casData.LastName;
var email = casData.Mail.First(s => s.EndsWith("@student.pg.edu.pl"));
var albumNumber = int.Parse(casData.AlbumNumber);
return Student.CreateStudent(id, firstName, lastName, email, albumNumber);
}
}
}

View File

@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using InternshipSystem.Api.Options;
using Microsoft.Extensions.Options;
namespace InternshipSystem.Api.Controllers
{
public class GutCasClient
{
private readonly HttpClient _client;
private readonly SecurityOptions _securityOptions;
public GutCasClient(HttpClient client, IOptions<SecurityOptions> options)
{
_securityOptions = options.Value;
client.BaseAddress = _securityOptions.BaseUrl;
_client = client;
}
public async Task<string> GetCasTokenAsync(string code, CancellationToken cancellationToken)
{
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "authorization_code" },
{ "client_id", _securityOptions.ClientId },
{ "client_secret", _securityOptions.Secret },
{ "redirect_uri", _securityOptions.RedirectUrl.ToString() },
{ "code", code }
}),
RequestUri = _securityOptions.TokenPath
};
var response = await _client.SendAsync(request, cancellationToken);
await using var stream = await response.Content.ReadAsStreamAsync();
var value = await JsonSerializer.DeserializeAsync<Dictionary<string, object>>(stream);
return value["access_token"].ToString();
}
public async Task<CasUserData> GetProfileAsync(string token, CancellationToken cancellationToken)
{
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
Content = new StringContent(string.Empty),
RequestUri = _securityOptions.ProfilePath
};
request.Headers.Authorization = AuthenticationHeaderValue.Parse($"Bearer {token}");
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
var response = await _client.SendAsync(request, cancellationToken);
await using var stream = await response.Content.ReadAsStreamAsync();
var result = await JsonSerializer.DeserializeAsync<CasUserProfile>(
stream,
new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return result.Attributes;
}
}
}

View File

@ -2,10 +2,15 @@
using System.Threading.Tasks;
using InternshipSystem.Api.Queries;
using InternshipSystem.Api.Security;
using InternshipSystem.Core.Commands;
using InternshipSystem.Repository;
using Microsoft.AspNetCore.Authorization;
using InternshipSystem.Api.Security;
using InternshipSystem.Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace InternshipSystem.Api.Controllers
{
@ -22,7 +27,7 @@ namespace InternshipSystem.Api.Controllers
/// <summary>
/// Validate and add filled internship registration form
/// </summary>
/// <param name="registrationQuery">Internship registration data</param>
/// <param name="updateRegistration">Internship registration data</param>
/// <response code="200">If registration form was successfully added</response>
/// <response code="400">If the provided registration query was malformed</response>
/// <response code="401">This action is only available for authorized student registered for current edition</response>

View File

@ -23,7 +23,6 @@
<ItemGroup>
<ProjectReference Include="../InternshipSystem.Core/InternshipSystem.Core.csproj" />
<ProjectReference Include="../InternshipSystem.Repository/InternshipSystem.Repository.csproj" />
<ProjectReference Include="..\AspNet.Security.OAuth.MyGut\AspNet.Security.OAuth.MyGut.csproj" />
</ItemGroup>
</Project>

View File

@ -1,8 +1,21 @@
namespace InternshipSystem.Api.Options
using System;
using System.Security.Policy;
namespace InternshipSystem.Api.Options
{
public class SecurityOptions
{
public string Secret { get; set; }
public double Expiration { get; set; }
public Uri BaseUrl { get; set; }
public Uri TokenPath { get; set; }
public Uri ProfilePath { get; set; }
public Uri RedirectUrl { get; set; }
public string ClientId { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace InternshipSystem.Api.Controllers
{
public class CasUserData
{
public string AlbumNumber { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<string> Mail { get; set; }
public string PersonNumber { get; set; }
public List<string> Pg_Cui_Portalroles { get; set; }
}
}

View File

@ -0,0 +1,10 @@
namespace InternshipSystem.Api.Controllers
{
public class CasUserProfile
{
public string Service { get; set; }
public CasUserData Attributes { get; set; }
public string Id { get; set; }
public string Client_Id { get; set; }
}
}

View File

@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Reflection;
using AutoMapper;
using InternshipSystem.Api.Controllers;
using InternshipSystem.Api.Extensions;
using InternshipSystem.Api.ModelBinders;
using InternshipSystem.Api.Options;
@ -26,32 +27,38 @@ namespace InternshipSystem.Api
public Startup(IConfiguration configuration) =>
Configuration = configuration;
public void ConfigureServices(IServiceCollection services) =>
public void ConfigureServices(IServiceCollection services)
{
services
.Configure<SecurityOptions>(Configuration.GetSection("SecurityOptions"))
.AddDbContext<InternshipDbContext>(o => o.UseNpgsql(Configuration.GetConnectionString("InternshipDatabase")))
.AddSwaggerGen(options =>
.Configure<SecurityOptions>(Configuration.GetSection("SecurityOptions"));
services
.AddStudentAuthentication()
.AddAuthorization(o =>
{
o.AddPolicy(Policies.RegisteredOnly, policy => policy.RequireClaim("Edition"));
})
.AddHttpClient<GutCasClient>();
services
.AddDbContext<InternshipDbContext>(o =>
o.UseNpgsql(Configuration.GetConnectionString("InternshipDatabase")))
.AddScoped<DatabaseFiller>()
.AddScoped<IInternshipService, InternshipService>()
.AddScoped<JwtTokenService>()
.AddAutoMapper(cfg => cfg.AddProfile<ApiProfile>());
services
.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo {Title = "InternshipSystem Api - TEST", Version = "v1"});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
})
.AddScoped<DatabaseFiller>()
.AddScoped<IInternshipService, InternshipService>()
.AddScoped<JwtTokenService>()
.AddAutoMapper(cfg => cfg.AddProfile<ApiProfile>())
.AddStudentAuthentication()
.AddAuthorization(o =>
{
o.AddPolicy(Policies.RegisteredOnly, policy => policy.RequireClaim("Edition"));
})
.AddControllers(o =>
{
o.ModelBinderProviders.Insert(0, new UserBinderProvider());
})
;
.AddControllers(o => { o.ModelBinderProviders.Insert(0, new UserBinderProvider()); });
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())

View File

@ -0,0 +1,33 @@
using System;
using InternshipSystem.Core.Entity.Internship;
namespace InternshipSystem.Core.Commands
{
public class UpdateRegistrationForm
{
public UpdateCompany? Company { get; set; }
public DateTime? Start { get; set; }
public DateTime? End { get; set; }
public InternshipType? Type { get; set; }
}
public struct UpdateCompany
{
public long? Id { get; set; }
public string Nip { get; set; }
public string Name { get; set; }
public UpdateBranchOffice? BranchOffice { get; set; }
public bool IsUpdate => Id.HasValue;
}
public struct UpdateBranchOffice
{
public long? Id { get; set; }
public string Street { get; set; }
public string Building { get; set; }
public string City { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
}
}

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using InternshipSystem.Core.Commands;
namespace InternshipSystem.Core
{
@ -24,5 +26,10 @@ namespace InternshipSystem.Core
{
Branches.Add(createBranch);
}
public static Company CreateCompany(UpdateCompany updateCompany)
{
throw new NotImplementedException();
}
}
}

View File

@ -14,6 +14,9 @@ namespace InternshipSystem.Core
public DateTime ReportingStart { get; set; }
public Course Course { get; set; }
public List<Internship> Internships { get; set; }
public InternshipType AllowedInternshipTypes { get; set; }
public List<EditionSubject> AvailableSubjects { get; set; }
public List<InternshipType> AvailableInternshipTypes { get; set; }
@ -40,5 +43,15 @@ namespace InternshipSystem.Core
Internships.Add(internship);
}
public bool IsDateDuringEdition(DateTime start, DateTime end)
{
return start >= EditionStart && end <= EditionFinish;
}
public bool IsInternshiptypeAllowed(InternshipType internshipType)
{
return AllowedInternshipTypes.HasFlag(internshipType);
}
}
}

View File

@ -1,5 +1,8 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using InternshipSystem.Core.Commands;
using InternshipSystem.Core.ValueObject;
namespace InternshipSystem.Core
{
@ -12,9 +15,10 @@ namespace InternshipSystem.Core
public Report Report { get; set; }
public List<Document> Approvals { get; set; }
public List<Document> Documentation { get; set; }
public float? Grade { get; set; }
public Edition Edition { get; set; }
public float? Grade { get; set; }
public void UpdateDocument(Document document)
{
@ -47,5 +51,38 @@ namespace InternshipSystem.Core
return internship;
}
public void UpdateInternshipRegistration(UpdateRegistrationForm updateRegistration)
{
var start = updateRegistration.Start ?? InternshipRegistration.Start;
var end = updateRegistration.End ?? InternshipRegistration.End;
if (!Edition.IsDateDuringEdition(start, end))
{
throw new ArgumentOutOfRangeException(nameof(InternshipRegistration.Start) + nameof(InternshipRegistration.End),"Date outside of edition boundaries");
}
var internshipType = updateRegistration.Type ?? InternshipRegistration.Type;
if (!Edition.IsInternshiptypeAllowed(internshipType))
{
throw new ArgumentException("Internship type not allowed for this edition", nameof(updateRegistration.Type));
}
var company = InternshipRegistration.Company;
if (company == null)
{
if (!updateRegistration.Company.HasValue)
{
throw new ArgumentException("Company");
}
company = Company.CreateCompany(updateRegistration.Company.Value);
}
InternshipRegistration.Update(start, end, internshipType);
}
}
}

View File

@ -17,6 +17,13 @@ namespace InternshipSystem.Core
{
return new InternshipRegistration();
}
public void Update(DateTime start, DateTime end, InternshipType internshipType)
{
Start = start;
End = end;
Type = internshipType;
}
public void UpdateCompany(Company newCompany)
{

View File

@ -9,6 +9,17 @@ namespace InternshipSystem.Core
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public int Semester { get; set; }
public static Student CreateStudent(long id, string firstName, string lastName, string email, in int albumNumber)
{
return new Student
{
Id = id,
AlbumNumber = albumNumber,
FirstName = firstName,
LastName = lastName,
Email = email
};
}
}
}

View File

@ -223,7 +223,6 @@ namespace InternshipSystem.Repository
LastName = "Kowalski",
AlbumNumber = 123456,
Email = "s123456@student.pg.edu.pl",
Semester = 4,
},
InternshipRegistration = new InternshipRegistration
{
@ -273,7 +272,6 @@ namespace InternshipSystem.Repository
LastName = "Kołek",
AlbumNumber = 102137,
Email = "s102137@student.pg.edu.pl",
Semester = 6,
},
InternshipRegistration = new InternshipRegistration
{

View File

@ -6,6 +6,10 @@
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\InternshipSystem.Api\InternshipSystem.Api.csproj" />
</ItemGroup>
<Import Project="../../props/Directory.Tests.Build.props" />
</Project>

View File

@ -1,6 +1,59 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using InternshipSystem.Api.Controllers;
using Machine.Specifications;
namespace InternshipSystem.Api.Test
{
[Subject(typeof(JsonSerializer))]
class When_deserializing_cas
{
private static string json;
private Establish context = () =>
{
// {
// "service": "https://system-praktyk.stg.kadet.net/user/login/code",
// "attributes": {
// "albumNumber": "165581",
// "firstName": "Kacper",
// "lastName": "Donat",
// "mail": [
// "kacdonat@pg.edu.pl",
// "kacper.donat@pg.edu.pl",
// "s165581@student.pg.edu.pl"
// ],
// "personNumber": "1101074",
// "PG_CUI_PORTALROLES": [
// "ROLE_EKONTAKT_PROD",
// "RP_STUDENT",
// "RP_USER",
// "ROLE_TRAC",
// "ROLE_HUDSON",
// "ROLE_WWW_ADMIN",
// "RP_PRACOWNIK"
// ]
// },
// "id": "1101074",
// "client_id": "PraktykiClientId"
// }
json =
" {\r\n \"service\": \"https://system-praktyk.stg.kadet.net/user/login/code\",\r\n \"attributes\": {\r\n \"albumNumber\": \"165581\",\r\n \"firstName\": \"Kacper\",\r\n \"lastName\": \"Donat\",\r\n \"mail\": [\r\n \"kacdonat@pg.edu.pl\",\r\n \"kacper.donat@pg.edu.pl\",\r\n \"s165581@student.pg.edu.pl\"\r\n ],\r\n \"personNumber\": \"1101074\",\r\n \"PG_CUI_PORTALROLES\": [\r\n \"ROLE_EKONTAKT_PROD\",\r\n \"RP_STUDENT\",\r\n \"RP_USER\",\r\n \"ROLE_TRAC\",\r\n \"ROLE_HUDSON\",\r\n \"ROLE_WWW_ADMIN\",\r\n \"RP_PRACOWNIK\"\r\n ]\r\n },\r\n \"id\": \"1101074\",\r\n \"client_id\": \"PraktykiClientId\"\r\n }";
options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
};
private Because of = () => result = JsonSerializer.Deserialize<CasUserProfile>(json, options);
private It should_nop = () => true.ShouldBeTrue();
private static JsonSerializerOptions options;
private static CasUserProfile result;
}
}