1
- using Microsoft . AspNetCore . Authorization ;
2
- using Microsoft . AspNetCore . Mvc ;
1
+ using Microsoft . AspNetCore . Mvc ;
3
2
using Microsoft . Extensions . Options ;
4
3
using Octokit ;
5
4
using Octokit . Internal ;
6
5
using System . Security . Claims ;
7
- using System . Text ;
8
6
9
7
namespace rubberduckvba . Server . Api . Auth ;
10
8
11
9
public record class UserViewModel
12
10
{
13
- public static UserViewModel Anonymous { get ; } = new UserViewModel { Name = "(anonymous)" , HasOrgRole = false } ;
11
+ public static UserViewModel Anonymous { get ; } = new UserViewModel { Name = "(anonymous)" , IsAuthenticated = false , IsAdmin = false } ;
14
12
15
13
public string Name { get ; init ; } = default ! ;
16
- public bool HasOrgRole { get ; init ; }
14
+ public bool IsAuthenticated { get ; init ; }
15
+ public bool IsAdmin { get ; init ; }
17
16
}
18
17
19
-
18
+ public record class SignInViewModel
19
+ {
20
+ public string ? State { get ; init ; }
21
+ public string ? Code { get ; init ; }
22
+ public string ? Token { get ; init ; }
23
+ }
20
24
21
25
[ ApiController ]
22
- [ AllowAnonymous ]
23
- public class AuthController ( IOptions < GitHubSettings > configuration , IOptions < ApiSettings > api ) : ControllerBase
26
+ public class AuthController ( IOptions < GitHubSettings > configuration , IOptions < ApiSettings > api , ILogger < AuthController > logger ) : ControllerBase
24
27
{
25
28
[ HttpGet ( "auth" ) ]
26
- [ AllowAnonymous ]
27
- public ActionResult < UserViewModel > Index ( )
29
+ public IActionResult Index ( )
28
30
{
29
31
var claims = HttpContext . User . Claims . ToDictionary ( claim => claim . Type , claim => claim . Value ) ;
30
32
var hasName = claims . TryGetValue ( ClaimTypes . Name , out var name ) ;
@@ -37,10 +39,12 @@ public ActionResult<UserViewModel> Index()
37
39
return BadRequest ( ) ;
38
40
}
39
41
42
+ var isAuthenticated = HttpContext . User . Identity ? . IsAuthenticated ?? false ;
40
43
var model = new UserViewModel
41
44
{
42
45
Name = name ,
43
- HasOrgRole = ( HttpContext . User . Identity ? . IsAuthenticated ?? false ) && role == configuration . Value . OwnerOrg
46
+ IsAuthenticated = isAuthenticated ,
47
+ IsAdmin = role == configuration . Value . OwnerOrg
44
48
} ;
45
49
46
50
return Ok ( model ) ;
@@ -52,12 +56,13 @@ public ActionResult<UserViewModel> Index()
52
56
}
53
57
54
58
[ HttpPost ( "auth/signin" ) ]
55
- [ AllowAnonymous ]
56
- public async Task < ActionResult > SignIn ( )
59
+ public IActionResult SessionSignIn ( SignInViewModel vm )
57
60
{
58
- var xsrf = Guid . NewGuid ( ) . ToString ( ) ;
59
- HttpContext . Session . SetString ( "xsrf:state" , xsrf ) ;
60
- await HttpContext . Session . CommitAsync ( ) ;
61
+ if ( User . Identity ? . IsAuthenticated ?? false )
62
+ {
63
+ logger . LogInformation ( "Signin was requested, but user is already authenticated. Redirecting to home page..." ) ;
64
+ return Redirect ( "/" ) ;
65
+ }
61
66
62
67
var clientId = configuration . Value . ClientId ;
63
68
var agent = configuration . Value . UserAgent ;
@@ -67,53 +72,45 @@ public async Task<ActionResult> SignIn()
67
72
{
68
73
AllowSignup = false ,
69
74
Scopes = { "read:user" , "read:org" } ,
70
- State = xsrf
75
+ State = vm . State
71
76
} ;
72
77
78
+ logger . LogInformation ( "Requesting OAuth app GitHub login url..." ) ;
73
79
var url = github . Oauth . GetGitHubLoginUrl ( request ) ;
74
80
if ( url is null )
75
81
{
82
+ logger . LogInformation ( "OAuth login was cancelled by the user or did not return a url." ) ;
76
83
return Forbid ( ) ;
77
84
}
78
85
79
- // TODO log url
80
- //return Redirect(url.ToString());
81
- return RedirectToAction ( "Index" , "Home" ) ;
86
+ logger . LogInformation ( "Returning the login url for the client to redirect. State: {xsrf}" , vm . State ) ;
87
+ return Ok ( url . ToString ( ) ) ;
82
88
}
83
89
84
- [ HttpGet ( "auth/github" ) ]
85
- [ AllowAnonymous ]
86
- public async Task < ActionResult > GitHubCallback ( string code , string state )
90
+ [ HttpPost ( "auth/github" ) ]
91
+ public async Task < IActionResult > OnGitHubCallback ( SignInViewModel vm )
87
92
{
88
- if ( string . IsNullOrWhiteSpace ( code ) )
89
- {
90
- return BadRequest ( ) ;
91
- }
92
-
93
- var expected = HttpContext . Session . GetString ( "xsrf:state" ) ;
94
- HttpContext . Session . Clear ( ) ;
95
- await HttpContext . Session . CommitAsync ( ) ;
96
-
97
- if ( state != expected )
98
- {
99
- return BadRequest ( ) ;
100
- }
101
-
93
+ logger . LogInformation ( "OAuth token was received. State: {state}" , vm . State ) ;
102
94
var clientId = configuration . Value . ClientId ;
103
95
var clientSecret = configuration . Value . ClientSecret ;
104
96
var agent = configuration . Value . UserAgent ;
105
97
106
98
var github = new GitHubClient ( new ProductHeaderValue ( agent ) ) ;
107
99
108
- var request = new OauthTokenRequest ( clientId , clientSecret , code ) ;
100
+ var request = new OauthTokenRequest ( clientId , clientSecret , vm . Code ) ;
109
101
var token = await github . Oauth . CreateAccessToken ( request ) ;
102
+ if ( token is null )
103
+ {
104
+ logger . LogWarning ( "OAuth access token was not created." ) ;
105
+ return Unauthorized ( ) ;
106
+ }
110
107
111
- await AuthorizeAsync ( token . AccessToken ) ;
112
-
113
- return Ok ( ) ;
108
+ logger . LogInformation ( "OAuth access token was created. Authorizing..." ) ;
109
+ var authorizedToken = await AuthorizeAsync ( token . AccessToken ) ;
110
+ return authorizedToken is null ? Unauthorized ( ) : Ok ( vm with { Token = authorizedToken } ) ;
114
111
}
115
112
116
- private async Task AuthorizeAsync ( string token )
113
+ private async Task < string ? > AuthorizeAsync ( string token )
117
114
{
118
115
try
119
116
{
@@ -122,42 +119,44 @@ private async Task AuthorizeAsync(string token)
122
119
var githubUser = await github . User . Current ( ) ;
123
120
if ( githubUser . Suspended )
124
121
{
125
- throw new InvalidOperationException ( "User is suspended" ) ;
122
+ logger . LogWarning ( "User {name} with login '{login}' ({url}) is a suspended GitHub account and will not be authorized." , githubUser . Name , githubUser . Login , githubUser . Url ) ;
123
+ return default ;
126
124
}
127
125
128
- var emailClaim = new Claim ( ClaimTypes . Email , githubUser . Email ) ;
129
-
130
126
var identity = new ClaimsIdentity ( "github" , ClaimTypes . Name , ClaimTypes . Role ) ;
131
- if ( identity != null )
132
- {
133
- identity . AddClaim ( new Claim ( ClaimTypes . Name , githubUser . Login ) ) ;
134
-
135
- var orgs = await github . Organization . GetAllForUser ( githubUser . Login ) ;
136
- var rdOrg = orgs . SingleOrDefault ( org => org . Id == configuration . Value . RubberduckOrgId ) ;
127
+ identity . AddClaim ( new Claim ( ClaimTypes . Name , githubUser . Login ) ) ;
128
+ logger . LogInformation ( "Creating claims identity for GitHub login '{login}'..." , githubUser . Login ) ;
137
129
138
- if ( rdOrg != null )
139
- {
140
- identity . AddClaim ( new Claim ( ClaimTypes . Role , configuration . Value . OwnerOrg ) ) ;
141
- identity . AddClaim ( new Claim ( ClaimTypes . Authentication , token ) ) ;
142
- identity . AddClaim ( new Claim ( "access_token" , token ) ) ;
130
+ var orgs = await github . Organization . GetAllForUser ( githubUser . Login ) ;
131
+ var rdOrg = orgs . SingleOrDefault ( org => org . Id == configuration . Value . RubberduckOrgId ) ;
143
132
144
- var principal = new ClaimsPrincipal ( identity ) ;
133
+ if ( rdOrg != null )
134
+ {
135
+ identity . AddClaim ( new Claim ( ClaimTypes . Role , configuration . Value . OwnerOrg ) ) ;
136
+ identity . AddClaim ( new Claim ( ClaimTypes . Authentication , token ) ) ;
137
+ identity . AddClaim ( new Claim ( "access_token" , token ) ) ;
138
+ logger . LogDebug ( "GitHub Organization claims were granted. Creating claims principal..." ) ;
145
139
146
- var issued = DateTime . UtcNow ;
147
- var expires = issued . Add ( TimeSpan . FromMinutes ( 50 ) ) ;
148
- var roles = string . Join ( "," , identity . Claims . Where ( claim => claim . Type == ClaimTypes . Role ) . Select ( claim => claim . Value ) ) ;
140
+ var principal = new ClaimsPrincipal ( identity ) ;
141
+ var roles = string . Join ( "," , identity . Claims . Where ( claim => claim . Type == ClaimTypes . Role ) . Select ( claim => claim . Value ) ) ;
149
142
150
- HttpContext . User = principal ;
151
- Thread . CurrentPrincipal = HttpContext . User ;
143
+ HttpContext . User = principal ;
144
+ Thread . CurrentPrincipal = HttpContext . User ;
152
145
153
- var jwt = principal . AsJWT ( api . Value . SymetricKey , configuration . Value . JwtIssuer , configuration . Value . JwtAudience ) ;
154
- HttpContext . Session . SetString ( "jwt" , jwt ) ;
155
- }
146
+ logger . LogInformation ( "GitHub user with login {login} has signed in with role authorizations '{role}'." , githubUser . Login , configuration . Value . OwnerOrg ) ;
147
+ return token ;
148
+ }
149
+ else
150
+ {
151
+ logger . LogWarning ( "User {name} ({email}) with login '{login}' is not a member of organization ID {org} and will not be authorized." , githubUser . Name , githubUser . Email , githubUser . Login , configuration . Value . RubberduckOrgId ) ;
152
+ return default ;
156
153
}
157
154
}
158
155
catch ( Exception )
159
156
{
160
157
// just ignore: configuration needs the org (prod) client app id to avoid throwing this exception
158
+ logger . LogWarning ( "An exception was thrown. Verify GitHub:ClientId and GitHub:ClientSecret configuration; authorization fails." ) ;
159
+ return default ;
161
160
}
162
161
}
163
162
}
0 commit comments