1
- using System ;
1
+ using System . Linq ;
2
2
using System . Collections . Generic ;
3
- using System . Linq ;
4
3
using System . Threading . Tasks ;
5
4
using Microsoft . AspNetCore . Http ;
6
5
using Microsoft . AspNetCore . Mvc ;
7
6
using Microsoft . AspNetCore . Authorization ;
8
7
using Microsoft . EntityFrameworkCore ;
9
- using TodoListAPI . Models ;
10
- using System . Security . Claims ;
8
+ using Microsoft . Identity . Web ;
11
9
using Microsoft . Identity . Web . Resource ;
10
+ using TodoListAPI . Models ;
12
11
13
12
namespace TodoListAPI . Controllers
14
13
{
@@ -17,70 +16,139 @@ namespace TodoListAPI.Controllers
17
16
[ ApiController ]
18
17
public class TodoListController : ControllerBase
19
18
{
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
-
24
19
private readonly TodoContext _context ;
25
20
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
+
26
26
public TodoListController ( TodoContext context )
27
27
{
28
28
_context = context ;
29
29
}
30
30
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
+
31
50
// GET: api/TodoItems
32
51
[ 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
+ ) ]
33
69
public async Task < ActionResult < IEnumerable < TodoItem > > > GetTodoItems ( )
34
70
{
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
+ }
38
91
}
39
92
40
93
// GET: api/TodoItems/5
41
94
[ HttpGet ( "{id}" ) ]
95
+ [ RequiredScopeOrAppPermission (
96
+ AcceptedScope = new string [ ] { _todoListRead , _todoListReadWrite } ,
97
+ AcceptedAppPermission = new string [ ] { _todoListReadAll , _todoListReadWriteAll }
98
+ ) ]
42
99
public async Task < ActionResult < TodoItem > > GetTodoItem ( int id )
43
100
{
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 ( ) )
49
104
{
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 ) ;
51
110
}
52
-
53
- return todoItem ;
54
111
}
55
112
56
113
// PUT: api/TodoItems/5
57
114
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
58
115
// more details see https://aka.ms/RazorPagesCRUD.
59
116
[ HttpPut ( "{id}" ) ]
117
+ [ RequiredScopeOrAppPermission (
118
+ AcceptedScope = new string [ ] { _todoListReadWrite } ,
119
+ AcceptedAppPermission = new string [ ] { _todoListReadWriteAll }
120
+ ) ]
60
121
public async Task < IActionResult > PutTodoItem ( int id , TodoItem todoItem )
61
122
{
62
- HttpContext . VerifyUserHasAnyAcceptedScope ( scopeRequiredByApi ) ;
63
-
64
- if ( id != todoItem . Id )
123
+ if ( id != todoItem . Id || ! _context . TodoItems . Any ( x => x . Id == id ) )
65
124
{
66
- return BadRequest ( ) ;
125
+ return NotFound ( ) ;
67
126
}
68
127
69
- _context . Entry ( todoItem ) . State = EntityState . Modified ;
70
128
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 ( ) )
76
132
{
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 ( ) ) )
82
134
{
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
+ }
84
152
}
85
153
}
86
154
@@ -91,10 +159,20 @@ public async Task<IActionResult> PutTodoItem(int id, TodoItem todoItem)
91
159
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
92
160
// more details see https://aka.ms/RazorPagesCRUD.
93
161
[ HttpPost ]
162
+ [ RequiredScopeOrAppPermission (
163
+ AcceptedScope = new string [ ] { _todoListReadWrite } ,
164
+ AcceptedAppPermission = new string [ ] { _todoListReadWriteAll }
165
+ ) ]
94
166
public async Task < ActionResult < TodoItem > > PostTodoItem ( TodoItem todoItem )
95
167
{
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
+
98
176
todoItem . Owner = owner ;
99
177
todoItem . Status = false ;
100
178
@@ -106,25 +184,28 @@ public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
106
184
107
185
// DELETE: api/TodoItems/5
108
186
[ HttpDelete ( "{id}" ) ]
187
+ [ RequiredScopeOrAppPermission (
188
+ AcceptedScope = new string [ ] { _todoListReadWrite } ,
189
+ AcceptedAppPermission = new string [ ] { _todoListReadWriteAll }
190
+ ) ]
109
191
public async Task < ActionResult < TodoItem > > DeleteTodoItem ( int id )
110
192
{
111
- HttpContext . VerifyUserHasAnyAcceptedScope ( scopeRequiredByApi ) ;
193
+ TodoItem todoItem = await _context . TodoItems . FindAsync ( id ) ;
112
194
113
- var todoItem = await _context . TodoItems . FindAsync ( id ) ;
114
195
if ( todoItem == null )
115
196
{
116
197
return NotFound ( ) ;
117
198
}
118
199
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
+ }
124
207
125
- private bool TodoItemExists ( int id )
126
- {
127
- return _context . TodoItems . Any ( e => e . Id == id ) ;
208
+ return NoContent ( ) ;
128
209
}
129
210
}
130
211
}
0 commit comments