add Gut authentication and authorization

This commit is contained in:
MaxchilKH 2020-09-12 23:01:10 +02:00
parent 86b240902d
commit 46d178a722
18 changed files with 266 additions and 136 deletions

View File

@ -7,8 +7,13 @@ services:
CONNECTIONSTRINGS__INTERNSHIPDATABASE: "Host=db.postgres;Port=5432;Database=postgres;Username=postgres;Password=szwoniu" CONNECTIONSTRINGS__INTERNSHIPDATABASE: "Host=db.postgres;Port=5432;Database=postgres;Username=postgres;Password=szwoniu"
ASPNETCORE_ENVIRONMENT: Development ASPNETCORE_ENVIRONMENT: Development
ASPNETCORE_URLS: http://+:80 ASPNETCORE_URLS: http://+:80
SECURITYOPTIONS__SECRET: PDv7DrqznYL6nv7DrqzjnQYO9JxIsWdcjnQYL6nu0f SECURITYOPTIONS__SECRET: iewaiwie3aig9wi3chieBai9eephai
SECURITYOPTIONS__EXPIRATION: 20 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: depends_on:
- db.postgres - db.postgres
ports: 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,22 @@
using System; using System;
using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using InternshipSystem.Api.Options; using InternshipSystem.Api.Options;
using InternshipSystem.Api.Security; using InternshipSystem.Api.Security;
using InternshipSystem.Core;
using InternshipSystem.Repository; using InternshipSystem.Repository;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Serilog;
namespace InternshipSystem.Api.Controllers namespace InternshipSystem.Api.Controllers
{ {
@ -19,12 +26,18 @@ namespace InternshipSystem.Api.Controllers
{ {
private readonly InternshipDbContext _context; private readonly InternshipDbContext _context;
private readonly JwtTokenService _tokenService; private readonly JwtTokenService _tokenService;
private readonly GutCasClient _loginClient;
private readonly SecurityOptions _securityOptions; 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; _context = context;
_tokenService = tokenService; _tokenService = tokenService;
_loginClient = loginClient;
_securityOptions = options.Value; _securityOptions = options.Value;
} }
@ -32,16 +45,47 @@ namespace InternshipSystem.Api.Controllers
[HttpGet("login")] [HttpGet("login")]
public async Task<ActionResult> Authenticate(string code, CancellationToken cancellationToken) 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[] var identity = new ClaimsIdentity(new[]
{ {
new Claim(ClaimTypes.Name, "Jan"), new Claim(ClaimTypes.Name, student.FirstName),
new Claim(ClaimTypes.Surname, "Kowalski"), new Claim(ClaimTypes.Surname, student.LastName),
new Claim(InternshipClaims.PersonNumber, "1") new Claim(InternshipClaims.PersonNumber, student.Id.ToString())
}); });
return Ok(_tokenService.generateToken(identity)); 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")] [HttpGet("loginEdition")]
[Authorize] [Authorize]
public async Task<ActionResult> LoginIntoEdition(Guid editionId, User user, CancellationToken token) public async Task<ActionResult> LoginIntoEdition(Guid editionId, User user, CancellationToken token)
@ -64,5 +108,80 @@ namespace InternshipSystem.Api.Controllers
return Ok(_tokenService.generateToken(newIdentity)); 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);
}
}
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

@ -23,7 +23,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="../InternshipSystem.Core/InternshipSystem.Core.csproj" /> <ProjectReference Include="../InternshipSystem.Core/InternshipSystem.Core.csproj" />
<ProjectReference Include="../InternshipSystem.Repository/InternshipSystem.Repository.csproj" /> <ProjectReference Include="../InternshipSystem.Repository/InternshipSystem.Repository.csproj" />
<ProjectReference Include="..\AspNet.Security.OAuth.MyGut\AspNet.Security.OAuth.MyGut.csproj" />
</ItemGroup> </ItemGroup>
</Project> </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 class SecurityOptions
{ {
public string Secret { get; set; } public string Secret { get; set; }
public double Expiration { 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.IO;
using System.Reflection; using System.Reflection;
using AutoMapper; using AutoMapper;
using InternshipSystem.Api.Controllers;
using InternshipSystem.Api.Extensions; using InternshipSystem.Api.Extensions;
using InternshipSystem.Api.ModelBinders; using InternshipSystem.Api.ModelBinders;
using InternshipSystem.Api.Options; using InternshipSystem.Api.Options;
@ -26,32 +27,38 @@ namespace InternshipSystem.Api
public Startup(IConfiguration configuration) => public Startup(IConfiguration configuration) =>
Configuration = configuration; Configuration = configuration;
public void ConfigureServices(IServiceCollection services) => public void ConfigureServices(IServiceCollection services)
{
services services
.Configure<SecurityOptions>(Configuration.GetSection("SecurityOptions")) .Configure<SecurityOptions>(Configuration.GetSection("SecurityOptions"));
.AddDbContext<InternshipDbContext>(o => o.UseNpgsql(Configuration.GetConnectionString("InternshipDatabase")))
.AddSwaggerGen(options => 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"}); options.SwaggerDoc("v1", new OpenApiInfo {Title = "InternshipSystem Api - TEST", Version = "v1"});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath); options.IncludeXmlComments(xmlPath);
}) })
.AddScoped<DatabaseFiller>() .AddControllers(o => { o.ModelBinderProviders.Insert(0, new UserBinderProvider()); });
.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());
})
;
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{ {
if (env.IsDevelopment()) if (env.IsDevelopment())

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using InternshipSystem.Core.Commands; using InternshipSystem.Core.Commands;
namespace InternshipSystem.Core namespace InternshipSystem.Core
@ -28,6 +29,7 @@ namespace InternshipSystem.Core
public static Company CreateCompany(UpdateCompany updateCompany) public static Company CreateCompany(UpdateCompany updateCompany)
{ {
throw new NotImplementedException();
} }
} }
} }

View File

@ -9,6 +9,17 @@ namespace InternshipSystem.Core
public string FirstName { get; set; } public string FirstName { get; set; }
public string LastName { get; set; } public string LastName { get; set; }
public string Email { 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

@ -162,7 +162,6 @@ namespace InternshipSystem.Repository
LastName = "Kowalski", LastName = "Kowalski",
AlbumNumber = 123456, AlbumNumber = 123456,
Email = "s123456@student.pg.edu.pl", Email = "s123456@student.pg.edu.pl",
Semester = 4,
}, },
InternshipRegistration = new InternshipRegistration InternshipRegistration = new InternshipRegistration
{ {
@ -212,12 +211,11 @@ namespace InternshipSystem.Repository
LastName = "Kołek", LastName = "Kołek",
AlbumNumber = 102137, AlbumNumber = 102137,
Email = "s102137@student.pg.edu.pl", Email = "s102137@student.pg.edu.pl",
Semester = 6,
}, },
InternshipRegistration = new InternshipRegistration InternshipRegistration = new InternshipRegistration
{ {
Company = Context.Companies.First(c => c.Id.Equals(2)), //Asseco Company = Context.Companies.First(c => c.Id.Equals(2)), //Asseco
Type = InternshipType.UOZ, Type = InternshipType.UZ,
Start = new DateTime(2000, 7, 1), Start = new DateTime(2000, 7, 1),
End = new DateTime(2000, 8, 30), End = new DateTime(2000, 8, 30),
State = DocumentState.Submitted, State = DocumentState.Submitted,

View File

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

View File

@ -1,6 +1,59 @@
using System; using System;
using System.Collections.Generic;
using System.Text.Json;
using InternshipSystem.Api.Controllers;
using Machine.Specifications; using Machine.Specifications;
namespace InternshipSystem.Api.Test 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;
}
} }