Доброго всем дня, уважаемые хабровчане!
До сего момента я являлся лишь читателем этого замечательного ресурса, но вот кажется и пришло время написать мою первую статью.
Oauth 2.1 — дальнейшее развитие популярного фреймворка авторизации Oauth 2.0, который на момент написания статьи всё ещё вроде как находится в стадии черновика. Но тем не менее уже начинает применяться. На хабре уже есть более подробная статья на эту тему.
Из не очень приятного, из Oauth 2.1 убраны варианты получения токена:
-
implict
-
password
Но взамен мы получаем поддержку PKCE как для публичных клиентов, так и для приватных.
И вот хочу вынести на ваш суд небольшой пример реализации получения токенов на spring authorization server (на момент написания статьи версия 0.3.1) и SPA на Vue.js.
Немного кода:
@Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("browser-client") .clientSecret("{noop}secret") .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri("http://127.0.0.1:8081/code") .scope(OidcScopes.OPENID) .scope("browser.read") .build(); return new InMemoryRegisteredClientRepository(registeredClient); }
На сервере регистрируем клиента и в первую очередь интересует нас вот эта строка .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
она настраивает то, каким способом будет авторизоваться наш браузерный клиент, в случае NONE, авторизация клиента не требуется, но в этом случае, будут выданы только access_token, и если необходимо id_token, refresh_token в случае публичного клиента выдаваться не будет.
Теперь код клиента:
login() { var codeVerifier = this.generateRandomString(64); Promise.resolve() .then(() => { return this.generateCodeChallenge(codeVerifier) }) .then(function(codeChallenge) { window.sessionStorage.setItem("code_verifier", codeVerifier) let args = new URLSearchParams({ response_type: "code", client_id: 'browser-client', redirect_uri: 'http://127.0.0.1:8081/code', state: '1234zyx', code_challenge: codeChallenge, code_challenge_method: 'S256', scope: 'openid browser.read' }); window.location = "http://127.0.0.1:9000/oauth2/authorize?" + args; }); }, async generateCodeChallenge(codeVerifier) { var digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier)); return btoa(String.fromCharCode(...new Uint8Array(digest))) .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') }, generateRandomString(length) { var text = ""; var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (var i = 0; i < length; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; }
Формируем url для перехода на сервер авторизации, тут всё стандартно, разве что не нужно указывать client_secret, а вместо него формируются 2 поля code_challenge и code_challenge_method. code_challenge — альфанумерик произвольная строка и code_challenge_method — метод её шифрования. Они будут запомнены на сервере и при обмене кода доступа на токен будут проверяться.
Так же нам в браузере необходимо сохранить исходную строку window.sessionStorage.setItem(«code_verifier», codeVerifier), в запросе обмена кода на токен эта строка так же будет отправляться на сервер и будет там сверена с отправленными ранее code_challenge и code_challenge_method. Вот собственно вторая часть кода, обмен кода доступа на токен:
router.beforeEach((to, from, next) => { if (to.path == '/code' && to.query.code != null) { let formData = new FormData() formData.append('grant_type','authorization_code') formData.append('code',to.query.code) formData.append('redirect_uri','http://127.0.0.1:8081/code') formData.append('client_id','browser-client') formData.append('code_verifier',window.sessionStorage.getItem("code_verifier")) axios.post('http://127.0.0.1:9000/oauth2/token', formData, { headers: { 'Content-type':'application/url-form-encoded' } } ).then(resp => { console.log(resp.data) window.sessionStorage.setItem("_a", resp.data.access_token); }) next({name: 'Index'}) } else { next() } })
Так как я использовал Vue.js и vue-router перехватом вызова занимается непосредственно роутер. И так если у нас произошёл вызов с путём /code и в запросе присутствует параметр code, роутер его перехватит, сформирует форму и отправит её на эндпоинт обмена кода на токен и в ответ мы получим собственно access_token (и id_token если у нас на сервере настроен .scope(OidcScopes.OPENID) и в первом запросе в скопах есть scope: ‘openid’).
Теперь немного нюансов.
Если на сервере у нас метод авторизации клиента .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC), а в запросе обмена кода на токен мы добавим заголовок ‘Authorization’:’Basic ‘+btoa(‘browser-client:secret’), то наш клиент становится конфиденциальным и в этом случае кроме access_token мы так же получим и refresh_token. Но как говорит нам спецификация, рефреш токен не должен храниться в браузере, так как нет способа гарантированно хранить его там безопасно.
Весь код можно посмотреть на GitHub.
На этом пожалуй всё, Надеюсь статья будет кому то полезна и интересна.
Спасибо!
ссылка на оригинал статьи https://habr.com/ru/post/688680/
Добавить комментарий