diff --git a/.docker/docker-compose.yaml b/.docker/docker-compose.yaml index 5e6599f..73c579b 100644 --- a/.docker/docker-compose.yaml +++ b/.docker/docker-compose.yaml @@ -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: diff --git a/src/AspNet.Security.OAuth.MyGut/AspNet.Security.OAuth.MyGut.csproj b/src/AspNet.Security.OAuth.MyGut/AspNet.Security.OAuth.MyGut.csproj deleted file mode 100644 index e5cfdb1..0000000 --- a/src/AspNet.Security.OAuth.MyGut/AspNet.Security.OAuth.MyGut.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - netcoreapp3.1 - - - - - - - diff --git a/src/AspNet.Security.OAuth.MyGut/AuthenticationBuilderExtension.cs b/src/AspNet.Security.OAuth.MyGut/AuthenticationBuilderExtension.cs deleted file mode 100644 index b442239..0000000 --- a/src/AspNet.Security.OAuth.MyGut/AuthenticationBuilderExtension.cs +++ /dev/null @@ -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 configuration - ) => - builder.AddOAuth(scheme, caption, configuration); - } -} \ No newline at end of file diff --git a/src/AspNet.Security.OAuth.MyGut/MyGutAuthenticationConstants.cs b/src/AspNet.Security.OAuth.MyGut/MyGutAuthenticationConstants.cs deleted file mode 100644 index 7edb2e5..0000000 --- a/src/AspNet.Security.OAuth.MyGut/MyGutAuthenticationConstants.cs +++ /dev/null @@ -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"; - } - } -} \ No newline at end of file diff --git a/src/AspNet.Security.OAuth.MyGut/MyGutAuthenticationDefaults.cs b/src/AspNet.Security.OAuth.MyGut/MyGutAuthenticationDefaults.cs deleted file mode 100644 index 5e07b0d..0000000 --- a/src/AspNet.Security.OAuth.MyGut/MyGutAuthenticationDefaults.cs +++ /dev/null @@ -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"; - } -} \ No newline at end of file diff --git a/src/AspNet.Security.OAuth.MyGut/MyGutAuthenticationHandler.cs b/src/AspNet.Security.OAuth.MyGut/MyGutAuthenticationHandler.cs deleted file mode 100644 index 6782093..0000000 --- a/src/AspNet.Security.OAuth.MyGut/MyGutAuthenticationHandler.cs +++ /dev/null @@ -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 - { - public MyGutAuthenticationHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock) : base(options, logger, encoder, clock) - { - } - } -} \ No newline at end of file diff --git a/src/AspNet.Security.OAuth.MyGut/MyGutAuthenticationOptions.cs b/src/AspNet.Security.OAuth.MyGut/MyGutAuthenticationOptions.cs deleted file mode 100644 index c3f027d..0000000 --- a/src/AspNet.Security.OAuth.MyGut/MyGutAuthenticationOptions.cs +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/src/InternshipSystem.Api/Controllers/AccessController.cs b/src/InternshipSystem.Api/Controllers/AccessController.cs index 44d1b95..d30c6fa 100644 --- a/src/InternshipSystem.Api/Controllers/AccessController.cs +++ b/src/InternshipSystem.Api/Controllers/AccessController.cs @@ -1,15 +1,22 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; using System.Security.Claims; +using System.Text.Json; 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 +26,18 @@ namespace InternshipSystem.Api.Controllers { private readonly InternshipDbContext _context; private readonly JwtTokenService _tokenService; + private readonly GutCasClient _loginClient; private readonly SecurityOptions _securityOptions; - public AccessController(IOptions options, InternshipDbContext context, JwtTokenService tokenService) + public AccessController( + IOptions options, + InternshipDbContext context, + JwtTokenService tokenService, + GutCasClient loginClient) { _context = context; _tokenService = tokenService; + _loginClient = loginClient; _securityOptions = options.Value; } @@ -32,16 +45,47 @@ namespace InternshipSystem.Api.Controllers [HttpGet("login")] public async Task 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 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 LoginIntoEdition(Guid editionId, User user, CancellationToken token) @@ -64,5 +108,80 @@ 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); + } + } + + public class GutCasClient + { + private readonly HttpClient _client; + private readonly SecurityOptions _securityOptions; + + public GutCasClient(HttpClient client, IOptions options) + { + _securityOptions = options.Value; + + client.BaseAddress = _securityOptions.BaseUrl; + _client = client; + } + + public async Task GetCasTokenAsync(string code, CancellationToken cancellationToken) + { + var request = new HttpRequestMessage + { + Method = HttpMethod.Post, + Content = new FormUrlEncodedContent(new Dictionary + { + { "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>(stream); + + return value["access_token"].ToString(); + } + + + public async Task 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( + stream, + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return result.Attributes; + } } } \ No newline at end of file diff --git a/src/InternshipSystem.Api/InternshipSystem.Api.csproj b/src/InternshipSystem.Api/InternshipSystem.Api.csproj index 133fa45..d5c17c0 100644 --- a/src/InternshipSystem.Api/InternshipSystem.Api.csproj +++ b/src/InternshipSystem.Api/InternshipSystem.Api.csproj @@ -23,7 +23,6 @@ - diff --git a/src/InternshipSystem.Api/Options/SecurityOptions.cs b/src/InternshipSystem.Api/Options/SecurityOptions.cs index 2fcf5fe..499b604 100644 --- a/src/InternshipSystem.Api/Options/SecurityOptions.cs +++ b/src/InternshipSystem.Api/Options/SecurityOptions.cs @@ -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; } } } \ No newline at end of file diff --git a/src/InternshipSystem.Api/Result/CasUserData.cs b/src/InternshipSystem.Api/Result/CasUserData.cs new file mode 100644 index 0000000..560a3ac --- /dev/null +++ b/src/InternshipSystem.Api/Result/CasUserData.cs @@ -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 Mail { get; set; } + public string PersonNumber { get; set; } + public List Pg_Cui_Portalroles { get; set; } + } +} \ No newline at end of file diff --git a/src/InternshipSystem.Api/Result/CasUserProfile.cs b/src/InternshipSystem.Api/Result/CasUserProfile.cs new file mode 100644 index 0000000..f051b2d --- /dev/null +++ b/src/InternshipSystem.Api/Result/CasUserProfile.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/InternshipSystem.Api/Startup.cs b/src/InternshipSystem.Api/Startup.cs index ab34720..ccdc27d 100644 --- a/src/InternshipSystem.Api/Startup.cs +++ b/src/InternshipSystem.Api/Startup.cs @@ -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(Configuration.GetSection("SecurityOptions")) - .AddDbContext(o => o.UseNpgsql(Configuration.GetConnectionString("InternshipDatabase"))) - .AddSwaggerGen(options => + .Configure(Configuration.GetSection("SecurityOptions")); + + services + .AddStudentAuthentication() + .AddAuthorization(o => + { + o.AddPolicy(Policies.RegisteredOnly, policy => policy.RequireClaim("Edition")); + }) + .AddHttpClient(); + + services + .AddDbContext(o => + o.UseNpgsql(Configuration.GetConnectionString("InternshipDatabase"))) + .AddScoped() + .AddScoped() + .AddScoped() + .AddAutoMapper(cfg => cfg.AddProfile()); + + 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() - .AddScoped() - .AddScoped() - .AddAutoMapper(cfg => cfg.AddProfile()) - .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()) diff --git a/src/InternshipSystem.Core/Entity/Company.cs b/src/InternshipSystem.Core/Entity/Company.cs index 15c3843..48201fd 100644 --- a/src/InternshipSystem.Core/Entity/Company.cs +++ b/src/InternshipSystem.Core/Entity/Company.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using InternshipSystem.Core.Commands; namespace InternshipSystem.Core @@ -28,6 +29,7 @@ namespace InternshipSystem.Core public static Company CreateCompany(UpdateCompany updateCompany) { + throw new NotImplementedException(); } } } \ No newline at end of file diff --git a/src/InternshipSystem.Core/Entity/Student.cs b/src/InternshipSystem.Core/Entity/Student.cs index b3e7fab..04b7f1b 100644 --- a/src/InternshipSystem.Core/Entity/Student.cs +++ b/src/InternshipSystem.Core/Entity/Student.cs @@ -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 + }; + } } } \ No newline at end of file diff --git a/src/InternshipSystem.Repository/DatabaseFiller.cs b/src/InternshipSystem.Repository/DatabaseFiller.cs index 4751c2d..6936d86 100644 --- a/src/InternshipSystem.Repository/DatabaseFiller.cs +++ b/src/InternshipSystem.Repository/DatabaseFiller.cs @@ -162,7 +162,6 @@ namespace InternshipSystem.Repository LastName = "Kowalski", AlbumNumber = 123456, Email = "s123456@student.pg.edu.pl", - Semester = 4, }, InternshipRegistration = new InternshipRegistration { @@ -212,12 +211,11 @@ namespace InternshipSystem.Repository LastName = "Kołek", AlbumNumber = 102137, Email = "s102137@student.pg.edu.pl", - Semester = 6, }, InternshipRegistration = new InternshipRegistration { Company = Context.Companies.First(c => c.Id.Equals(2)), //Asseco - Type = InternshipType.UOZ, + Type = InternshipType.UZ, Start = new DateTime(2000, 7, 1), End = new DateTime(2000, 8, 30), State = DocumentState.Submitted, diff --git a/test/InternshipSystem.Api.Test/InternshipSystem.Api.Test.csproj b/test/InternshipSystem.Api.Test/InternshipSystem.Api.Test.csproj index 80e5e31..b18b329 100644 --- a/test/InternshipSystem.Api.Test/InternshipSystem.Api.Test.csproj +++ b/test/InternshipSystem.Api.Test/InternshipSystem.Api.Test.csproj @@ -6,6 +6,10 @@ false + + + + diff --git a/test/InternshipSystem.Api.Test/InternshipSystem.cs b/test/InternshipSystem.Api.Test/InternshipSystem.cs index b490e1d..2033085 100644 --- a/test/InternshipSystem.Api.Test/InternshipSystem.cs +++ b/test/InternshipSystem.Api.Test/InternshipSystem.cs @@ -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(json, options); + + private It should_nop = () => true.ShouldBeTrue(); + + private static JsonSerializerOptions options; + private static CasUserProfile result; + } }