using JobsJobsJobs.Server.Models; using JobsJobsJobs.Shared; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using System; using System.IdentityModel.Tokens.Jwt; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Security.Claims; using System.Text; using System.Text.Json; using System.Threading.Tasks; namespace JobsJobsJobs.Server { /// /// Authentication / authorization utility methods /// public static class Auth { /// /// Verify the authorization code with Mastodon and get the user's profile /// /// The code from the authorization flow /// The authorization configuration section /// The Mastodon account (or an error if one is encountered) public static async Task> VerifyWithMastodon(string authCode, IConfigurationSection config) { using var http = new HttpClient(); // Use authorization code to get an access token from NAS using var codeResult = await http.PostAsJsonAsync("https://noagendasocial.com/oauth/token", new { client_id = config["ClientId"], client_secret = config["Secret"], redirect_uri = $"{config["ReturnHost"]}/citizen/authorized", grant_type = "authorization_code", code = authCode, scope = "read" }); if (!codeResult.IsSuccessStatusCode) { Console.WriteLine($"ERR: {await codeResult.Content.ReadAsStringAsync()}"); return Result.AsError( $"Could not get token ({codeResult.StatusCode:D}: {codeResult.ReasonPhrase})"); } using var tokenResponse = JsonSerializer.Deserialize( new ReadOnlySpan(await codeResult.Content.ReadAsByteArrayAsync())); if (tokenResponse == null) { return Result.AsError("Could not parse authorization code result"); } var accessToken = tokenResponse.RootElement.GetProperty("access_token").GetString(); // Use access token to get profile from NAS using var req = new HttpRequestMessage(HttpMethod.Get, $"{config["ApiUrl"]}accounts/verify_credentials"); req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); using var profileResult = await http.SendAsync(req); if (!profileResult.IsSuccessStatusCode) { return Result.AsError( $"Could not get profile ({profileResult.StatusCode:D}: {profileResult.ReasonPhrase})"); } var profileResponse = JsonSerializer.Deserialize( new ReadOnlySpan(await profileResult.Content.ReadAsByteArrayAsync())); if (profileResponse == null) { return Result.AsError("Could not parse profile result"); } if (profileResponse.Username != profileResponse.AccountName) { return Result.AsError( $"Profiles must be from noagendasocial.com; yours is {profileResponse.AccountName}"); } return Result.AsOk(profileResponse); } /// /// Create a JSON Web Token for this citizen to use for further requests to this API /// /// The citizen for which the token should be generated /// The authorization configuration section /// The JWT public static string CreateJwt(Citizen citizen, IConfigurationSection config) { var tokenHandler = new JwtSecurityTokenHandler(); var token = tokenHandler.CreateToken(new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, citizen.Id.ToString()), new Claim(ClaimTypes.Name, citizen.CitizenName), }), Expires = DateTime.UtcNow.AddHours(2), Issuer = "https://noagendacareers.com", Audience = "https://noagendacareers.com", SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["ServerSecret"])), SecurityAlgorithms.HmacSha256Signature) }); return tokenHandler.WriteToken(token); } } }