1- using System ;
2- using System . Collections . Generic ;
1+ using System . Collections . Generic ;
32using System . Linq ;
3+ using System . Security . Claims ;
44using System . Threading . Tasks ;
5+ using Microsoft . AspNetCore . Authorization ;
56using Microsoft . AspNetCore . Http ;
67using Microsoft . AspNetCore . Mvc ;
7- using Microsoft . AspNetCore . Authorization ;
88using Microsoft . EntityFrameworkCore ;
9- using TodoListAPI . Models ;
10- using System . Security . Claims ;
9+ using Microsoft . Identity . Web ;
1110using Microsoft . Identity . Web . Resource ;
11+ using TodoListAPI . Models ;
1212
1313namespace TodoListAPI . Controllers
1414{
@@ -17,70 +17,98 @@ namespace TodoListAPI.Controllers
1717 [ ApiController ]
1818 public class TodoListController : ControllerBase
1919 {
20- // The Web API will only accept tokens 1) for users, and
21- // 2) having the demo.read scope for this API
22- static readonly string [ ] scopeRequiredByApi = new string [ ] { "demo.read" } ;
20+ private readonly TodoContext _TodoListContext ;
21+ private readonly IHttpContextAccessor _contextAccessor ;
22+ private ClaimsPrincipal _currentPrincipal ;
2323
24- private readonly TodoContext _context ;
24+ /// <summary>
25+ /// We store the object id of the user/app derived from the presented Access token
26+ /// </summary>
27+ private string _currentPrincipalId = string . Empty ;
2528
26- public TodoListController ( TodoContext context )
29+ public TodoListController ( TodoContext context , IHttpContextAccessor contextAccessor )
2730 {
28- _context = context ;
31+ _TodoListContext = context ;
32+ _contextAccessor = contextAccessor ;
33+
34+ // We seek the details of the user/app represented by the access token presented to this API, This can be empty unless authN succeeded
35+ // If a user signed-in, the value will be the unique identifier of the user.
36+ _currentPrincipal = GetCurrentClaimsPrincipal ( ) ;
37+
38+ if ( ! IsAppOnlyToken ( ) && _currentPrincipal != null )
39+ {
40+ _currentPrincipalId = _currentPrincipal . GetObjectId ( ) ;
41+ PopulateDefaultToDos ( _currentPrincipalId ) ;
42+ }
43+ }
44+
45+ // GET: api/todolist/getAll
46+ [ HttpGet ]
47+ [ Route ( "getAll" ) ]
48+ [ RequiredScope ( RequiredScopesConfigurationKey = "AzureAd:Scopes:Read" ) ]
49+ public async Task < ActionResult < IEnumerable < TodoItem > > > GetAll ( )
50+ {
51+ return await _TodoListContext . TodoItems . ToListAsync ( ) ;
2952 }
3053
3154 // GET: api/TodoItems
3255 [ HttpGet ]
56+ [ RequiredScope ( RequiredScopesConfigurationKey = "AzureAd:Scopes:Read" ) ]
3357 public async Task < ActionResult < IEnumerable < TodoItem > > > GetTodoItems ( )
3458 {
35- HttpContext . VerifyUserHasAnyAcceptedScope ( scopeRequiredByApi ) ;
36- string owner = User . FindFirst ( ClaimTypes . NameIdentifier ) ? . Value ;
37- return await _context . TodoItems . Where ( item => item . Owner == owner ) . ToListAsync ( ) ;
59+ /// <summary>
60+ /// The 'oid' (object id) is the only claim that should be used to uniquely identify
61+ /// a user in an Azure AD tenant. The token might have one or more of the following claim,
62+ /// that might seem like a unique identifier, but is not and should not be used as such:
63+ ///
64+ /// - upn (user principal name): might be unique amongst the active set of users in a tenant
65+ /// but tend to get reassigned to new employees as employees leave the organization and others
66+ /// take their place or might change to reflect a personal change like marriage.
67+ ///
68+ /// - email: might be unique amongst the active set of users in a tenant but tend to get reassigned
69+ /// to new employees as employees leave the organization and others take their place.
70+ /// </summary>
71+ return await _TodoListContext . TodoItems . Where ( x => x . Owner == _currentPrincipalId ) . ToListAsync ( ) ;
3872 }
3973
4074 // GET: api/TodoItems/5
4175 [ HttpGet ( "{id}" ) ]
76+ [ RequiredScope ( RequiredScopesConfigurationKey = "AzureAd:Scopes:Read" ) ]
4277 public async Task < ActionResult < TodoItem > > GetTodoItem ( int id )
4378 {
44- HttpContext . VerifyUserHasAnyAcceptedScope ( scopeRequiredByApi ) ;
45-
46- var todoItem = await _context . TodoItems . FindAsync ( id ) ;
47-
48- if ( todoItem == null )
49- {
50- return NotFound ( ) ;
51- }
52-
53- return todoItem ;
79+ return await _TodoListContext . TodoItems . FirstOrDefaultAsync ( t => t . Id == id && t . Owner == _currentPrincipalId ) ;
5480 }
5581
5682 // PUT: api/TodoItems/5
5783 // To protect from overposting attacks, please enable the specific properties you want to bind to, for
5884 // more details see https://aka.ms/RazorPagesCRUD.
5985 [ HttpPut ( "{id}" ) ]
86+ [ RequiredScope ( RequiredScopesConfigurationKey = "AzureAd:Scopes:Write" ) ]
6087 public async Task < IActionResult > PutTodoItem ( int id , TodoItem todoItem )
6188 {
62- HttpContext . VerifyUserHasAnyAcceptedScope ( scopeRequiredByApi ) ;
63-
64- if ( id != todoItem . Id )
89+ if ( id != todoItem . Id || ! _TodoListContext . TodoItems . Any ( x => x . Id == id ) )
6590 {
66- return BadRequest ( ) ;
91+ return NotFound ( ) ;
6792 }
6893
69- _context . Entry ( todoItem ) . State = EntityState . Modified ;
70-
71- try
94+ if ( _TodoListContext . TodoItems . Any ( x => x . Id == id && x . Owner == _currentPrincipalId ) )
7295 {
73- await _context . SaveChangesAsync ( ) ;
74- }
75- catch ( DbUpdateConcurrencyException )
76- {
77- if ( ! TodoItemExists ( id ) )
96+ _TodoListContext . Entry ( todoItem ) . State = EntityState . Modified ;
97+
98+ try
7899 {
79- return NotFound ( ) ;
100+ await _TodoListContext . SaveChangesAsync ( ) ;
80101 }
81- else
102+ catch ( DbUpdateConcurrencyException )
82103 {
83- throw ;
104+ if ( ! _TodoListContext . TodoItems . Any ( e => e . Id == id ) )
105+ {
106+ return NotFound ( ) ;
107+ }
108+ else
109+ {
110+ throw ;
111+ }
84112 }
85113 }
86114
@@ -91,40 +119,83 @@ public async Task<IActionResult> PutTodoItem(int id, TodoItem todoItem)
91119 // To protect from overposting attacks, please enable the specific properties you want to bind to, for
92120 // more details see https://aka.ms/RazorPagesCRUD.
93121 [ HttpPost ]
122+ [ RequiredScope ( RequiredScopesConfigurationKey = "AzureAd:Scopes:Write" ) ]
94123 public async Task < ActionResult < TodoItem > > PostTodoItem ( TodoItem todoItem )
95124 {
96- HttpContext . VerifyUserHasAnyAcceptedScope ( scopeRequiredByApi ) ;
97- string owner = User . FindFirst ( ClaimTypes . NameIdentifier ) ? . Value ;
98- todoItem . Owner = owner ;
125+ todoItem . Owner = _currentPrincipalId ;
99126 todoItem . Status = false ;
100127
101- _context . TodoItems . Add ( todoItem ) ;
102- await _context . SaveChangesAsync ( ) ;
128+ _TodoListContext . TodoItems . Add ( todoItem ) ;
129+ await _TodoListContext . SaveChangesAsync ( ) ;
103130
104131 return CreatedAtAction ( "GetTodoItem" , new { id = todoItem . Id } , todoItem ) ;
105132 }
106133
107134 // DELETE: api/TodoItems/5
108135 [ HttpDelete ( "{id}" ) ]
136+ [ RequiredScope ( RequiredScopesConfigurationKey = "AzureAd:Scopes:Write" ) ]
109137 public async Task < ActionResult < TodoItem > > DeleteTodoItem ( int id )
110138 {
111- HttpContext . VerifyUserHasAnyAcceptedScope ( scopeRequiredByApi ) ;
139+ TodoItem todoItem = await _TodoListContext . TodoItems . FindAsync ( id ) ;
112140
113- var todoItem = await _context . TodoItems . FindAsync ( id ) ;
114141 if ( todoItem == null )
115142 {
116143 return NotFound ( ) ;
117144 }
118145
119- _context . TodoItems . Remove ( todoItem ) ;
120- await _context . SaveChangesAsync ( ) ;
146+ if ( _TodoListContext . TodoItems . Any ( x => x . Id == id && x . Owner == _currentPrincipalId ) )
147+ {
148+ _TodoListContext . TodoItems . Remove ( todoItem ) ;
149+ await _TodoListContext . SaveChangesAsync ( ) ;
150+ }
151+
152+ return NoContent ( ) ;
153+ }
154+
155+ private async void PopulateDefaultToDos ( string _currentPrincipalId )
156+ {
157+ //Pre - populate with sample data
158+ if ( _TodoListContext . TodoItems . Count ( ) == 0 && ! string . IsNullOrEmpty ( _currentPrincipalId ) )
159+ {
160+ _TodoListContext . TodoItems . Add ( new TodoItem ( ) { Owner = $ "{ _currentPrincipalId } ", Description = "Pick up groceries" , Status = false } ) ;
161+ _TodoListContext . TodoItems . Add ( new TodoItem ( ) { Owner = $ "{ _currentPrincipalId } ", Description = "Finish invoice report" , Status = false } ) ;
162+
163+ await _TodoListContext . SaveChangesAsync ( ) ;
164+ }
165+ }
166+
167+ /// <summary>
168+ /// returns the current claimsPrincipal (user/Client app) dehydrated from the Access token
169+ /// </summary>
170+ /// <returns></returns>
171+ private ClaimsPrincipal GetCurrentClaimsPrincipal ( )
172+ {
173+ // Irrespective of whether a user signs in or not, the AspNet security middleware dehydrates
174+ // the claims in the HttpContext.User.Claims collection
175+ if ( _contextAccessor . HttpContext != null && _contextAccessor . HttpContext . User != null )
176+ {
177+ return _contextAccessor . HttpContext . User ;
178+ }
121179
122- return todoItem ;
180+ return null ;
123181 }
124182
125- private bool TodoItemExists ( int id )
183+ /// <summary>
184+ /// Indicates of the AT presented was for an app-only token or not.
185+ /// </summary>
186+ /// <returns></returns>
187+ private bool IsAppOnlyToken ( )
126188 {
127- return _context . TodoItems . Any ( e => e . Id == id ) ;
189+ // Add in the optional 'idtyp' claim to check if the access token is coming from an application or user.
190+ //
191+ // See: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims
192+
193+ if ( GetCurrentClaimsPrincipal ( ) != null )
194+ {
195+ return GetCurrentClaimsPrincipal ( ) . Claims . Any ( c => c . Type == "idtyp" && c . Value == "app" ) ;
196+ }
197+
198+ return false ;
128199 }
129200 }
130- }
201+ }
0 commit comments