Skip to content

Commit fafa238

Browse files
committed
Using dotnet new webapi to create the api skeleton of the project, swagger to generate the api document, and add put jwt token server and jwt authentication handler into one project.
1 parent 9ef1232 commit fafa238

12 files changed

+543
-0
lines changed

.DS_Store

6 KB
Binary file not shown.

.vscode/launch.json

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
// Use IntelliSense to find out which attributes exist for C# debugging
3+
// Use hover for the description of the existing attributes
4+
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": ".NET Core Launch (web)",
9+
"type": "coreclr",
10+
"request": "launch",
11+
"preLaunchTask": "build",
12+
// If you have changed target frameworks, make sure to update the program path.
13+
"program": "${workspaceRoot}/bin/Debug/netcoreapp1.1/ApiDemo.dll",
14+
"args": [],
15+
"cwd": "${workspaceRoot}",
16+
"stopAtEntry": false,
17+
"internalConsoleOptions": "openOnSessionStart",
18+
"launchBrowser": {
19+
"enabled": true,
20+
"args": "${auto-detect-url}",
21+
"windows": {
22+
"command": "cmd.exe",
23+
"args": "/C start ${auto-detect-url}"
24+
},
25+
"osx": {
26+
"command": "open"
27+
},
28+
"linux": {
29+
"command": "xdg-open"
30+
}
31+
},
32+
"env": {
33+
"ASPNETCORE_ENVIRONMENT": "Development"
34+
},
35+
"sourceFileMap": {
36+
"/Views": "${workspaceRoot}/Views"
37+
}
38+
},
39+
{
40+
"name": ".NET Core Attach",
41+
"type": "coreclr",
42+
"request": "attach",
43+
"processId": "${command:pickProcess}"
44+
}
45+
]
46+
}

.vscode/tasks.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"version": "0.1.0",
3+
"command": "dotnet",
4+
"isShellCommand": true,
5+
"args": [],
6+
"tasks": [
7+
{
8+
"taskName": "build",
9+
"args": [
10+
"${workspaceRoot}/ApiDemo.csproj"
11+
],
12+
"isBuildCommand": true,
13+
"problemMatcher": "$msCompile"
14+
}
15+
]
16+
}

ApiDemo.csproj

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<Project Sdk="Microsoft.NET.Sdk.Web">
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp1.1</TargetFramework>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<Folder Include="wwwroot\"/>
8+
</ItemGroup>
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.1"/>
11+
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2"/>
12+
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1"/>
13+
<PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0"/>
14+
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="5.1.3"/>
15+
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.1.3"/>
16+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="1.1.1"/>
17+
</ItemGroup>
18+
</Project>

Controllers/JwtController.cs

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using System;
2+
using System.IdentityModel.Tokens.Jwt;
3+
using System.Security.Claims;
4+
using System.Security.Principal;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Authorization;
7+
using Microsoft.AspNetCore.Mvc;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.Extensions.Options;
10+
using Newtonsoft.Json;
11+
12+
namespace ApiDemo {
13+
[Route("api/[controller]")]
14+
public class JwtController : Controller
15+
{
16+
private readonly JwtIssuerOptions _jwtOptions;
17+
private readonly ILogger _logger;
18+
private readonly JsonSerializerSettings _serializerSettings;
19+
20+
public JwtController(IOptions<JwtIssuerOptions> jwtOptions, ILoggerFactory loggerFactory)
21+
{
22+
_jwtOptions = jwtOptions.Value;
23+
ThrowIfInvalidOptions(_jwtOptions);
24+
25+
_logger = loggerFactory.CreateLogger<JwtController>();
26+
27+
_serializerSettings = new JsonSerializerSettings
28+
{
29+
Formatting = Formatting.Indented
30+
};
31+
}
32+
33+
[HttpPost]
34+
[AllowAnonymous]
35+
public async Task<IActionResult> Get([FromForm] ApplicationUser applicationUser)
36+
{
37+
var identity = await GetClaimsIdentity(applicationUser);
38+
if (identity == null)
39+
{
40+
_logger.LogInformation($"Invalid username ({applicationUser.UserName}) or password ({applicationUser.Password})");
41+
return BadRequest("Invalid credentials");
42+
}
43+
44+
var claims = new[]
45+
{
46+
new Claim(JwtRegisteredClaimNames.Sub, applicationUser.UserName),
47+
new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
48+
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
49+
identity.FindFirst("DisneyCharacter")
50+
};
51+
52+
// Create the JWT security token and encode it.
53+
var jwt = new JwtSecurityToken(
54+
issuer: _jwtOptions.Issuer,
55+
audience: _jwtOptions.Audience,
56+
claims: claims,
57+
notBefore: _jwtOptions.NotBefore,
58+
expires: _jwtOptions.Expiration,
59+
signingCredentials: _jwtOptions.SigningCredentials
60+
);
61+
62+
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
63+
64+
// Serialize and return the response
65+
var response = new
66+
{
67+
access_token = encodedJwt,
68+
expires_in = (int)_jwtOptions.ValidFor.TotalSeconds
69+
};
70+
71+
var json = JsonConvert.SerializeObject(response, _serializerSettings);
72+
return new OkObjectResult(json);
73+
}
74+
75+
private static void ThrowIfInvalidOptions(JwtIssuerOptions options)
76+
{
77+
if (options == null) throw new ArgumentNullException(nameof(options));
78+
79+
if (options.ValidFor <= TimeSpan.Zero)
80+
{
81+
throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(JwtIssuerOptions.ValidFor));
82+
}
83+
84+
if (options.SigningCredentials == null)
85+
{
86+
throw new ArgumentNullException(nameof(JwtIssuerOptions.SigningCredentials));
87+
}
88+
89+
if (options.JtiGenerator == null)
90+
{
91+
throw new ArgumentNullException(nameof(JwtIssuerOptions.JtiGenerator));
92+
}
93+
}
94+
95+
/// <returns>Date converted to seconds since Unix epoch (Jan 1, 1970, midnight UTC).</returns>
96+
private static long ToUnixEpochDate(DateTime date)
97+
=> (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1979, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
98+
99+
/// <summary>
100+
/// IMAGINE BIG RED WARNING SIGNS HERE!
101+
/// You'd want to retrieve claims through your claims provider
102+
/// in whatever way suits you, the below is purely for demo purposes!
103+
/// </summary>
104+
private static Task<ClaimsIdentity> GetClaimsIdentity(ApplicationUser user)
105+
{
106+
if (user.UserName == "MickeyMouse" && user.Password == "MickeyMouse123")
107+
{
108+
return Task.FromResult(new ClaimsIdentity(new GenericIdentity(user.UserName, "Token"),
109+
new[]
110+
{
111+
new Claim("DisneyCharacter", "IAmMickey")
112+
}
113+
));
114+
}
115+
116+
if (user.UserName == "NotMickeyMouse" && user.Password == "NotMickeyMouse123")
117+
{
118+
return Task.FromResult(new ClaimsIdentity(new GenericIdentity(user.UserName, "Token"),
119+
new Claim[] { }
120+
));
121+
}
122+
123+
// Credentials are invalid, or accout doesn't exist
124+
return Task.FromResult<ClaimsIdentity>(null);
125+
}
126+
}
127+
}

Controllers/ValuesController.cs

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.AspNetCore.Mvc;
7+
using Newtonsoft.Json;
8+
9+
namespace ApiDemo.Controllers
10+
{
11+
[Route("api/[controller]")]
12+
public class ValuesController : Controller
13+
{
14+
private readonly JsonSerializerSettings _serializerSettings;
15+
16+
public ValuesController()
17+
{
18+
_serializerSettings = new JsonSerializerSettings
19+
{
20+
Formatting = Formatting.Indented
21+
};
22+
}
23+
24+
// GET api/values
25+
[HttpGet]
26+
[Authorize(Policy = "DisneyUser")]
27+
public IEnumerable<Value> Get()
28+
{
29+
return new Value[] { new Value{Id = 1, Text = "value1" }, new Value{ Id = 2, Text="value2"} };
30+
}
31+
32+
// GET api/values/5
33+
[HttpGet("{id:int}")]
34+
public Value Get(int id)
35+
{
36+
return new Value { Id = id, Text = "value" };
37+
}
38+
39+
// POST api/values
40+
[HttpPost]
41+
public IActionResult Post([FromBody]Value value)
42+
{
43+
return CreatedAtAction("Get", new { id = value.Id }, value);
44+
}
45+
46+
// PUT api/values/5
47+
[HttpPut("{id}")]
48+
public void Put(int id, [FromBody]string value)
49+
{
50+
}
51+
52+
// DELETE api/values/5
53+
[HttpDelete("{id}")]
54+
public void Delete(int id)
55+
{
56+
}
57+
}
58+
59+
public class Value {
60+
public int Id { get; set; }
61+
public string Text { get; set; }
62+
}
63+
}

Models/ApplicationUser.cs

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace ApiDemo
2+
{
3+
public class ApplicationUser
4+
{
5+
public string UserName { get; set; }
6+
public string Password { get; set; }
7+
}
8+
}

Options/JwtIssuerOptions.cs

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Microsoft.IdentityModel.Tokens;
4+
5+
namespace ApiDemo
6+
{
7+
public class JwtIssuerOptions
8+
{
9+
/// <summary>
10+
/// "iss" (Issuer) Claim
11+
/// </summary>
12+
/// <remarks>The "iss" (issuer) claim identifies the principal that issued the
13+
/// JWT. The processing of this claim is generally application specific.
14+
/// The "iss" value is a case-sensitive string containing a StringOrURI
15+
/// value. Use of this claim is OPTIONAL.</remarks>
16+
public string Issuer { get; set; }
17+
18+
/// <summary>
19+
/// "sub" (Subject) Claim
20+
/// </summary>
21+
/// <remarks> The "sub" (subject) claim identifies the principal that is the
22+
/// subject of the JWT. The claims in a JWT are normally statements
23+
/// about the subject. The subject value MUST either be scoped to be
24+
/// locally unique in the context of the issuer or be globally unique.
25+
/// The processing of this claim is generally application specific. The
26+
/// "sub" value is a case-sensitive string containing a StringOrURI
27+
/// value. Use of this claim is OPTIONAL.</remarks>
28+
public string Subject { get; set; }
29+
30+
/// <summary>
31+
/// "aud" (Audience) Claim
32+
/// </summary>
33+
/// <remarks>The "aud" (audience) claim identifies the recipients that the JWT is
34+
/// intended for. Each principal intended to process the JWT MUST
35+
/// identify itself with a value in the audience claim. If the principal
36+
/// processing the claim does not identify itself with a value in the
37+
/// "aud" claim when this claim is present, then the JWT MUST be
38+
/// rejected. In the general case, the "aud" value is an array of case-
39+
/// sensitive strings, each containing a StringOrURI value. In the
40+
/// special case when the JWT has one audience, the "aud" value MAY be a
41+
/// single case-sensitive string containing a StringOrURI value. The
42+
/// interpretation of audience values is generally application specific.
43+
/// Use of this claim is OPTIONAL.</remarks>
44+
public string Audience { get; set; }
45+
46+
/// <summary>
47+
/// "nbf" (Not Before) Claim (default is UTC NOW)
48+
/// </summary>
49+
/// <remarks>The "nbf" (not before) claim identifies the time before which the JWT
50+
/// MUST NOT be accepted for processing. The processing of the "nbf"
51+
/// claim requires that the current date/time MUST be after or equal to
52+
/// the not-before date/time listed in the "nbf" claim. Implementers MAY
53+
/// provide for some small leeway, usually no more than a few minutes, to
54+
/// account for clock skew. Its value MUST be a number containing a
55+
/// NumericDate value. Use of this claim is OPTIONAL.</remarks>
56+
public DateTime NotBefore => DateTime.UtcNow;
57+
58+
/// <summary>
59+
/// "iat" (Issued At) Claim (default is UTC NOW)
60+
/// </summary>
61+
/// <remarks>The "iat" (issued at) claim identifies the time at which the JWT was
62+
/// issued. This claim can be used to determine the age of the JWT. Its
63+
/// value MUST be a number containing a NumericDate value. Use of this
64+
/// claim is OPTIONAL.</remarks>
65+
public DateTime IssuedAt => DateTime.UtcNow;
66+
67+
/// <summary>
68+
/// Set the timespan the token will be valid for (default is 5 min/300 seconds)
69+
/// </summary>
70+
public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(5);
71+
72+
/// <summary>
73+
/// "exp" (Expiration Time) Claim (returns IssuedAt + ValidFor)
74+
/// </summary>
75+
/// <remarks>The "exp" (expiration time) claim identifies the expiration time on
76+
/// or after which the JWT MUST NOT be accepted for processing. The
77+
/// processing of the "exp" claim requires that the current date/time
78+
/// MUST be before the expiration date/time listed in the "exp" claim.
79+
/// Implementers MAY provide for some small leeway, usually no more than
80+
/// a few minutes, to account for clock skew. Its value MUST be a number
81+
/// containing a NumericDate value. Use of this claim is OPTIONAL.</remarks>
82+
public DateTime Expiration => IssuedAt.Add(ValidFor);
83+
84+
/// <summary>
85+
/// "jti" (JWT ID) Claim (default ID is a GUID)
86+
/// </summary>
87+
/// <remarks>The "jti" (JWT ID) claim provides a unique identifier for the JWT.
88+
/// The identifier value MUST be assigned in a manner that ensures that
89+
/// there is a negligible probability that the same value will be
90+
/// accidentally assigned to a different data object; if the application
91+
/// uses multiple issuers, collisions MUST be prevented among values
92+
/// produced by different issuers as well. The "jti" claim can be used
93+
/// to prevent the JWT from being replayed. The "jti" value is a case-
94+
/// sensitive string. Use of this claim is OPTIONAL.</remarks>
95+
public Func<Task<string>> JtiGenerator =>
96+
() => Task.FromResult(Guid.NewGuid().ToString());
97+
98+
/// <summary>
99+
/// The signing key to use when generating tokens.
100+
/// </summary>
101+
public SigningCredentials SigningCredentials { get; set; }
102+
}
103+
}

0 commit comments

Comments
 (0)