Skip to content

Commit e65c0f6

Browse files
committed
Implemented redis cache for permission and jwt checking
1 parent 5d5a359 commit e65c0f6

File tree

16 files changed

+423
-56
lines changed

16 files changed

+423
-56
lines changed

client/src/App.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export const Navigation = () => {
3838
let history = useHistory();
3939

4040
const logOut = () => {
41-
dispatch({ type: Constants.LOGOUT_REQUEST });
41+
let data = { jti: userContext.jti };
42+
dispatch({ type: Constants.LOGOUT_REQUEST, payload: data });
4243
}
4344

4445
let login = () => {

client/src/constants/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export const Constants = {
33
LOGIN_SUCCESS: 'LOGIN_REQUEST_SUCCESS',
44
LOGIN_FAILURE: 'LOGIN_REQUEST_FAILURE',
55
LOGOUT_REQUEST: 'LOGOUT_REQUEST',
6+
LOGOUT_REQUEST_SUCCESS: 'LOGOUT_REQUEST_SUCCESS',
67
PERMISSION_SUCCESS: 'PERMISSION_SUCCESS',
78

89
REGISTER_REQUEST: 'REGISTER_REQUEST',

client/src/reducers/userReducer.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const initialState = {
77
isRegistered: false,
88
error: null,
99
role: null,
10-
resources: null
10+
resources: null,
11+
jti: null
1112
}
1213

1314
const getUser = (state, data) => {
@@ -17,7 +18,8 @@ const getUser = (state, data) => {
1718
user: { username: data.userName },
1819
token: data.access_token,
1920
role: data.role,
20-
resources: data.resources
21+
resources: data.resources,
22+
jti: data.jti
2123
};
2224
}
2325

@@ -35,7 +37,7 @@ export default (state = initialState, action) => {
3537
// role: data.role,
3638
// resources: data.resources
3739
// };
38-
case Constants.LOGOUT_REQUEST:
40+
case Constants.LOGOUT_REQUEST_SUCCESS:
3941
localStorage.removeItem('data');
4042
return {
4143
isAuthenticated: initialState.isAuthenticated,

client/src/sagas/api.js

+6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ axios.interceptors.response.use(function (response) {
2323
localStorage.removeItem('data');
2424
window.location = '/login';
2525
} else {
26+
window.location = '/';
2627
return Promise.reject(error.response);
2728
}
2829
});
@@ -69,6 +70,11 @@ export const login = (data) => {
6970
return axios.post(`${AuthUrl}/api/token`, data);
7071
}
7172

73+
export const logout = (data) => {
74+
console.log("logout api call ->", data);
75+
return axios.post(`${AuthUrl}/api/logout`, data);
76+
}
77+
7278
export const register = (data) => {
7379
console.log("register api call ->", data);
7480
return axios.post(`${AuthUrl}/api/user/register`, data);

client/src/sagas/loginSaga.js

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { call, put, takeEvery } from "redux-saga/effects";
2+
import * as api from './api';
3+
import { Constants } from "../constants";
4+
5+
export function* login({ payload }) {
6+
try {
7+
let output = yield call(api.login, payload);
8+
yield put({ type: 'LOGIN_REQUEST_SUCCESS', payload: output });
9+
} catch (error) {
10+
console.log('login error', error);
11+
}
12+
}
13+
14+
function* watchLogin() {
15+
yield takeEvery('LOGIN_REQUEST', login);
16+
}
17+
18+
export function* logout({ payload }) {
19+
try {
20+
let output = yield call(api.logout, payload);
21+
yield put({ type: 'LOGOUT_REQUEST_SUCCESS', payload: output });
22+
} catch (error) {
23+
console.log('login error', error);
24+
}
25+
}
26+
27+
function* watchLogout() {
28+
yield takeEvery('LOGOUT_REQUEST', logout);
29+
}
30+
31+
export function* register({ payload }) {
32+
try {
33+
let output = yield call(api.register, payload);
34+
yield put({ type: Constants.REGISTER_SUCCESS, payload: output });
35+
} catch (error) {
36+
console.log('register error', error);
37+
yield put({ type: Constants.REGISTER_FAILURE, payload: error });
38+
}
39+
}
40+
41+
function* watchRegister() {
42+
yield takeEvery(Constants.REGISTER_REQUEST, register);
43+
}
44+
45+
export default [
46+
watchLogin(),
47+
watchLogout(),
48+
watchRegister()
49+
];

client/src/sagas/postSaga.js

+1-30
Original file line numberDiff line numberDiff line change
@@ -97,41 +97,12 @@ function* watchFetchComments() {
9797
yield takeEvery('FETCH_COMMENTS', fetchComments);
9898
}
9999

100-
export function* login({ payload }) {
101-
try {
102-
let output = yield call(api.login, payload);
103-
yield put({ type: 'LOGIN_REQUEST_SUCCESS', payload: output });
104-
} catch (error) {
105-
console.log('login error', error);
106-
}
107-
}
108-
109-
function* watchLogin() {
110-
yield takeEvery('LOGIN_REQUEST', login);
111-
}
112-
113-
export function* register({ payload }) {
114-
try {
115-
let output = yield call(api.register, payload);
116-
yield put({ type: Constants.REGISTER_SUCCESS, payload: output });
117-
} catch (error) {
118-
console.log('register error', error);
119-
yield put({ type: Constants.REGISTER_FAILURE, payload: error });
120-
}
121-
}
122-
123-
function* watchRegister() {
124-
yield takeEvery(Constants.REGISTER_REQUEST, register);
125-
}
126-
127100
export default [
128101
watchAddPost(),
129102
watchEditPost(),
130103
watchDeletePost(),
131104
watchFetchPosts(),
132105
watchFetchPostDetail(),
133106
watchAddComment(),
134-
watchFetchComments(),
135-
watchLogin(),
136-
watchRegister()
107+
watchFetchComments()
137108
];

client/src/sagas/sagas.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { all } from "redux-saga/effects";
22
import posts from "./postSaga";
3+
import logins from "./loginSaga";
34
import resources from "./resourceSaga";
45
import roles from "./roleSaga";
56
import permissions from "./permissionSaga";
67

78
export default function* rootSaga() {
8-
let allSagas = [...posts, ...resources, ...roles, ...permissions];
9+
let allSagas = [...posts, ...logins, ...resources, ...roles, ...permissions];
910
yield all(allSagas);
1011
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.EntityFrameworkCore;
8+
using AuthWebApplication.Models;
9+
using AuthWebApplication.Models.Db;
10+
using AuthWebApplication.Services;
11+
using Newtonsoft.Json;
12+
using StackExchange.Redis;
13+
14+
namespace AuthWebApplication.Controllers
15+
{
16+
[Route("api/[controller]")]
17+
[ApiController]
18+
public class ApplicationUserTokensController : ControllerBase
19+
{
20+
private readonly SecurityDbContext _context;
21+
private readonly RedisService redisService;
22+
23+
public ApplicationUserTokensController(SecurityDbContext context, RedisService redisService)
24+
{
25+
_context = context;
26+
this.redisService = redisService;
27+
}
28+
29+
// GET: api/ApplicationUserTokens
30+
[HttpGet]
31+
public async Task<ActionResult<IEnumerable<ApplicationUserToken>>> GetApplicationUserTokens()
32+
{
33+
return await _context.ApplicationUserTokens.ToListAsync();
34+
}
35+
36+
[HttpGet]
37+
[Route("Search")]
38+
public async Task<ActionResult<List<object>>> SearchTokens(string keyword)
39+
{
40+
var redisValues = await redisService.SearchRedisKeys(keyword);
41+
42+
return Ok(redisValues);
43+
}
44+
45+
// GET: api/ApplicationUserTokens/5
46+
[HttpGet("{id}")]
47+
public async Task<ActionResult<ApplicationUserToken>> GetApplicationUserToken(string id)
48+
{
49+
var applicationUserToken = await _context.ApplicationUserTokens.FindAsync(id);
50+
51+
if (applicationUserToken == null)
52+
{
53+
return NotFound();
54+
}
55+
56+
return applicationUserToken;
57+
}
58+
59+
// PUT: api/ApplicationUserTokens/5
60+
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
61+
[HttpPut("{id}")]
62+
public async Task<IActionResult> PutApplicationUserToken(string id, ApplicationUserToken applicationUserToken)
63+
{
64+
if (id != applicationUserToken.UserId)
65+
{
66+
return BadRequest();
67+
}
68+
69+
_context.Entry(applicationUserToken).State = EntityState.Modified;
70+
71+
try
72+
{
73+
await _context.SaveChangesAsync();
74+
}
75+
catch (DbUpdateConcurrencyException)
76+
{
77+
if (!ApplicationUserTokenExists(id))
78+
{
79+
return NotFound();
80+
}
81+
else
82+
{
83+
throw;
84+
}
85+
}
86+
87+
return NoContent();
88+
}
89+
90+
// POST: api/ApplicationUserTokens
91+
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
92+
[HttpPost]
93+
public async Task<ActionResult<ApplicationUserToken>> PostApplicationUserToken(ApplicationUserToken applicationUserToken)
94+
{
95+
_context.ApplicationUserTokens.Add(applicationUserToken);
96+
try
97+
{
98+
await _context.SaveChangesAsync();
99+
}
100+
catch (DbUpdateException)
101+
{
102+
if (ApplicationUserTokenExists(applicationUserToken.UserId))
103+
{
104+
return Conflict();
105+
}
106+
else
107+
{
108+
throw;
109+
}
110+
}
111+
112+
return CreatedAtAction("GetApplicationUserToken", new { id = applicationUserToken.UserId }, applicationUserToken);
113+
}
114+
115+
// DELETE: api/ApplicationUserTokens/5
116+
[HttpDelete("{id}")]
117+
public async Task<IActionResult> DeleteApplicationUserToken(string id)
118+
{
119+
var applicationUserToken = await _context.ApplicationUserTokens.FindAsync(id);
120+
if (applicationUserToken == null)
121+
{
122+
return NotFound();
123+
}
124+
125+
_context.ApplicationUserTokens.Remove(applicationUserToken);
126+
await _context.SaveChangesAsync();
127+
128+
return NoContent();
129+
}
130+
131+
private bool ApplicationUserTokenExists(string id)
132+
{
133+
return _context.ApplicationUserTokens.Any(e => e.UserId == id);
134+
}
135+
}
136+
}

server/AuthWebApplication/AuthWebApplication/Controllers/AuthorizeTokenController.cs

+38-4
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Threading.Tasks;
5+
using AuthWebApplication.Models.ViewModels;
56
using AuthWebApplication.Services;
7+
using Microsoft.AspNetCore.Authentication;
8+
using Microsoft.AspNetCore.Authorization;
69
using Microsoft.AspNetCore.Http;
710
using Microsoft.AspNetCore.Mvc;
11+
using Newtonsoft.Json;
12+
using Newtonsoft.Json.Linq;
813

914
namespace AuthWebApplication.Controllers
1015
{
@@ -19,11 +24,40 @@ public AuthorizeTokenController(RedisService redisService)
1924
this.redisService = redisService;
2025
}
2126

22-
public async Task<IActionResult> Get(string jti)
27+
public async Task<IActionResult> Get(string userName, string jti, string resource)
2328
{
24-
var s = await redisService.Get(jti);
25-
var inValid = string.IsNullOrWhiteSpace(s);
26-
return inValid ? (IActionResult) Unauthorized(jti) : Ok();
29+
var inValid = string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(jti) || string.IsNullOrWhiteSpace(resource);
30+
if (inValid)
31+
{
32+
return Unauthorized("Invalid data");
33+
}
34+
35+
var redisValue = await redisService.Get(userName);
36+
if (string.IsNullOrWhiteSpace(redisValue))
37+
{
38+
return Unauthorized(userName);
39+
}
40+
41+
var dbValue = (dynamic)JsonConvert.DeserializeObject(redisValue);
42+
var jtiArray = ((dbValue as dynamic).jtis as dynamic) as JArray;
43+
var list = jtiArray.ToObject<List<string>>();
44+
var validJti = list.Exists(x => x == jti);
45+
46+
if (!validJti)
47+
{
48+
return Unauthorized(jti);
49+
}
50+
51+
var permissionViewModels = JsonConvert.DeserializeObject<List<ApplicationPermissionViewModel>>(
52+
((dbValue as dynamic).resources as JValue).ToString());
53+
var permitted = permissionViewModels.Exists(x => x.Name == resource && Convert.ToBoolean(x.IsAllowed));
54+
55+
if (!permitted)
56+
{
57+
return Forbid("Bearer");
58+
}
59+
60+
return Ok();
2761
}
2862
}
2963
}

0 commit comments

Comments
 (0)