1- using System ;
1+ using System . Linq ;
22using System . Collections . Generic ;
3- using System . Linq ;
43using System . Threading . Tasks ;
54using Microsoft . AspNetCore . Http ;
65using Microsoft . AspNetCore . Mvc ;
76using Microsoft . AspNetCore . Authorization ;
87using Microsoft . EntityFrameworkCore ;
9- using TodoListAPI . Models ;
10- using System . Security . Claims ;
8+ using Microsoft . Identity . Web ;
119using Microsoft . Identity . Web . Resource ;
10+ using TodoListAPI . Models ;
1211
1312namespace TodoListAPI . Controllers
1413{
@@ -17,70 +16,139 @@ namespace TodoListAPI.Controllers
1716 [ ApiController ]
1817 public class TodoListController : ControllerBase
1918 {
20- // The Web API will only accept tokens 1) for users, and
21- // 2) having the access_as_user scope for this API
22- static readonly string [ ] scopeRequiredByApi = new string [ ] { "access_as_user" } ;
23-
2419 private readonly TodoContext _context ;
2520
21+ private const string _todoListRead = "TodoList.Read" ;
22+ private const string _todoListReadWrite = "TodoList.ReadWrite" ;
23+ private const string _todoListReadAll = "TodoList.Read.All" ;
24+ private const string _todoListReadWriteAll = "TodoList.ReadWrite.All" ;
25+
2626 public TodoListController ( TodoContext context )
2727 {
2828 _context = context ;
2929 }
3030
31+ /// <summary>
32+ /// Indicates if the AT presented has application or delegated permissions.
33+ /// </summary>
34+ /// <returns></returns>
35+ private bool IsAppOnlyToken ( )
36+ {
37+ // Add in the optional 'idtyp' claim to check if the access token is coming from an application or user.
38+ // See: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims
39+ if ( HttpContext . User . Claims . Any ( c => c . Type == "idtyp" ) )
40+ {
41+ return HttpContext . User . Claims . Any ( c => c . Type == "idtyp" && c . Value == "app" ) ;
42+ }
43+ else
44+ {
45+ // alternatively, if an AT contains the roles claim but no scp claim, that indicates it's an app token
46+ return HttpContext . User . Claims . Any ( c => c . Type == "roles" ) && HttpContext . User . Claims . Any ( c => c . Type != "scp" ) ;
47+ }
48+ }
49+
3150 // GET: api/TodoItems
3251 [ HttpGet ]
52+ /// <summary>
53+ /// Access tokens that have neither the 'scp' (for delegated permissions) nor
54+ /// 'roles' (for application permissions) claim are not to be honored.
55+ ///
56+ /// An access token issued by Azure AD will have at least one of the two claims. Access tokens
57+ /// issued to a user will have the 'scp' claim. Access tokens issued to an application will have
58+ /// the roles claim. Access tokens that contain both claims are issued only to users, where the scp
59+ /// claim designates the delegated permissions, while the roles claim designates the user's role.
60+ ///
61+ /// To determine whether an access token was issued to a user (i.e delegated) or an application
62+ /// more easily, we recommend enabling the optional claim 'idtyp'. For more information, see:
63+ /// https://docs.microsoft.com/azure/active-directory/develop/access-tokens#user-and-application-tokens
64+ /// </summary>
65+ [ RequiredScopeOrAppPermission (
66+ AcceptedScope = new string [ ] { _todoListRead , _todoListReadWrite } ,
67+ AcceptedAppPermission = new string [ ] { _todoListReadAll , _todoListReadWriteAll }
68+ ) ]
3369 public async Task < ActionResult < IEnumerable < TodoItem > > > GetTodoItems ( )
3470 {
35- HttpContext . VerifyUserHasAnyAcceptedScope ( scopeRequiredByApi ) ;
36- string owner = User . FindFirst ( ClaimTypes . NameIdentifier ) ? . Value ;
37- return await _context . TodoItems . Where ( item => item . Owner == owner ) . ToListAsync ( ) ;
71+ if ( ! IsAppOnlyToken ( ) )
72+ {
73+ /// <summary>
74+ /// The 'oid' (object id) is the only claim that should be used to uniquely identify
75+ /// a user in an Azure AD tenant. The token might have one or more of the following claim,
76+ /// that might seem like a unique identifier, but is not and should not be used as such:
77+ ///
78+ /// - upn (user principal name): might be unique amongst the active set of users in a tenant
79+ /// but tend to get reassigned to new employees as employees leave the organization and others
80+ /// take their place or might change to reflect a personal change like marriage.
81+ ///
82+ /// - email: might be unique amongst the active set of users in a tenant but tend to get reassigned
83+ /// to new employees as employees leave the organization and others take their place.
84+ /// </summary>
85+ return await _context . TodoItems . Where ( x => x . Owner == HttpContext . User . GetObjectId ( ) ) . ToListAsync ( ) ;
86+ }
87+ else
88+ {
89+ return await _context . TodoItems . ToListAsync ( ) ;
90+ }
3891 }
3992
4093 // GET: api/TodoItems/5
4194 [ HttpGet ( "{id}" ) ]
95+ [ RequiredScopeOrAppPermission (
96+ AcceptedScope = new string [ ] { _todoListRead , _todoListReadWrite } ,
97+ AcceptedAppPermission = new string [ ] { _todoListReadAll , _todoListReadWriteAll }
98+ ) ]
4299 public async Task < ActionResult < TodoItem > > GetTodoItem ( int id )
43100 {
44- HttpContext . VerifyUserHasAnyAcceptedScope ( scopeRequiredByApi ) ;
45-
46- var todoItem = await _context . TodoItems . FindAsync ( id ) ;
47-
48- if ( todoItem == null )
101+ // if it only has delegated permissions, then it will be t.id==id && x.Owner == owner
102+ // if it has app permissions the it will return t.id==id
103+ if ( ! IsAppOnlyToken ( ) )
49104 {
50- return NotFound ( ) ;
105+ return await _context . TodoItems . FirstOrDefaultAsync ( t => t . Id == id && t . Owner == HttpContext . User . GetObjectId ( ) ) ;
106+ }
107+ else
108+ {
109+ return await _context . TodoItems . FirstOrDefaultAsync ( t => t . Id == id ) ;
51110 }
52-
53- return todoItem ;
54111 }
55112
56113 // PUT: api/TodoItems/5
57114 // To protect from overposting attacks, please enable the specific properties you want to bind to, for
58115 // more details see https://aka.ms/RazorPagesCRUD.
59116 [ HttpPut ( "{id}" ) ]
117+ [ RequiredScopeOrAppPermission (
118+ AcceptedScope = new string [ ] { _todoListReadWrite } ,
119+ AcceptedAppPermission = new string [ ] { _todoListReadWriteAll }
120+ ) ]
60121 public async Task < IActionResult > PutTodoItem ( int id , TodoItem todoItem )
61122 {
62- HttpContext . VerifyUserHasAnyAcceptedScope ( scopeRequiredByApi ) ;
63-
64- if ( id != todoItem . Id )
123+ if ( id != todoItem . Id || ! _context . TodoItems . Any ( x => x . Id == id ) )
65124 {
66- return BadRequest ( ) ;
125+ return NotFound ( ) ;
67126 }
68127
69- _context . Entry ( todoItem ) . State = EntityState . Modified ;
70128
71- try
72- {
73- await _context . SaveChangesAsync ( ) ;
74- }
75- catch ( DbUpdateConcurrencyException )
129+ if ( ( ! IsAppOnlyToken ( ) && _context . TodoItems . Any ( x => x . Id == id && x . Owner == HttpContext . User . GetObjectId ( ) ) )
130+ ||
131+ IsAppOnlyToken ( ) )
76132 {
77- if ( ! TodoItemExists ( id ) )
78- {
79- return NotFound ( ) ;
80- }
81- else
133+ if ( _context . TodoItems . Any ( x => x . Id == id && x . Owner == HttpContext . User . GetObjectId ( ) ) )
82134 {
83- throw ;
135+ _context . Entry ( todoItem ) . State = EntityState . Modified ;
136+
137+ try
138+ {
139+ await _context . SaveChangesAsync ( ) ;
140+ }
141+ catch ( DbUpdateConcurrencyException )
142+ {
143+ if ( ! _context . TodoItems . Any ( e => e . Id == id ) )
144+ {
145+ return NotFound ( ) ;
146+ }
147+ else
148+ {
149+ throw ;
150+ }
151+ }
84152 }
85153 }
86154
@@ -91,10 +159,20 @@ public async Task<IActionResult> PutTodoItem(int id, TodoItem todoItem)
91159 // To protect from overposting attacks, please enable the specific properties you want to bind to, for
92160 // more details see https://aka.ms/RazorPagesCRUD.
93161 [ HttpPost ]
162+ [ RequiredScopeOrAppPermission (
163+ AcceptedScope = new string [ ] { _todoListReadWrite } ,
164+ AcceptedAppPermission = new string [ ] { _todoListReadWriteAll }
165+ ) ]
94166 public async Task < ActionResult < TodoItem > > PostTodoItem ( TodoItem todoItem )
95167 {
96- HttpContext . VerifyUserHasAnyAcceptedScope ( scopeRequiredByApi ) ;
97- string owner = User . FindFirst ( ClaimTypes . NameIdentifier ) ? . Value ;
168+ string owner = HttpContext . User . GetObjectId ( ) ;
169+
170+ if ( IsAppOnlyToken ( ) )
171+ {
172+ // with such a permission any owner name is accepted
173+ owner = todoItem . Owner ;
174+ }
175+
98176 todoItem . Owner = owner ;
99177 todoItem . Status = false ;
100178
@@ -106,25 +184,28 @@ public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
106184
107185 // DELETE: api/TodoItems/5
108186 [ HttpDelete ( "{id}" ) ]
187+ [ RequiredScopeOrAppPermission (
188+ AcceptedScope = new string [ ] { _todoListReadWrite } ,
189+ AcceptedAppPermission = new string [ ] { _todoListReadWriteAll }
190+ ) ]
109191 public async Task < ActionResult < TodoItem > > DeleteTodoItem ( int id )
110192 {
111- HttpContext . VerifyUserHasAnyAcceptedScope ( scopeRequiredByApi ) ;
193+ TodoItem todoItem = await _context . TodoItems . FindAsync ( id ) ;
112194
113- var todoItem = await _context . TodoItems . FindAsync ( id ) ;
114195 if ( todoItem == null )
115196 {
116197 return NotFound ( ) ;
117198 }
118199
119- _context . TodoItems . Remove ( todoItem ) ;
120- await _context . SaveChangesAsync ( ) ;
121-
122- return todoItem ;
123- }
200+ if ( ( ! IsAppOnlyToken ( ) && _context . TodoItems . Any ( x => x . Id == id && x . Owner == HttpContext . User . GetObjectId ( ) ) )
201+ ||
202+ IsAppOnlyToken ( ) )
203+ {
204+ _context . TodoItems . Remove ( todoItem ) ;
205+ await _context . SaveChangesAsync ( ) ;
206+ }
124207
125- private bool TodoItemExists ( int id )
126- {
127- return _context . TodoItems . Any ( e => e . Id == id ) ;
208+ return NoContent ( ) ;
128209 }
129210 }
130211}
0 commit comments