Oauth 2.1 spring authorization server + SPA

от автора

Доброго всем дня, уважаемые хабровчане!

До сего момента я являлся лишь читателем этого замечательного ресурса, но вот кажется и пришло время написать мою первую статью.

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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *