Browse Source

авторизация по токену

kpmy 5 years ago
parent
commit
fb15d7e36c

+ 26 - 0
pom.xml

@@ -61,6 +61,12 @@
             <version>${spring.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+            <version>${spring.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>com.querydsl</groupId>
             <artifactId>querydsl-mongodb</artifactId>
@@ -156,6 +162,26 @@
             <version>3.10</version>
         </dependency>
 
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-api</artifactId>
+            <version>0.11.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-impl</artifactId>
+            <version>0.11.1</version>
+            <scope>runtime</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
+            <version>0.11.1</version>
+            <scope>runtime</scope>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 5 - 1
src/app/package.json

@@ -18,10 +18,14 @@
     "@angular/platform-browser": "~9.1.4",
     "@angular/platform-browser-dynamic": "~9.1.4",
     "@angular/router": "~9.1.4",
+    "@auth0/angular-jwt": "^4.0.0",
+    "moment": "^2.25.3",
     "rxjs": "~6.5.4",
     "tslib": "^1.10.0",
+    "underscore": "^1.10.2",
     "uuid": "^8.0.0",
-    "zone.js": "~0.10.2"
+    "zone.js": "~0.10.2",
+    "ngx-init": "^0.1.1"
   },
   "devDependencies": {
     "@angular-devkit/build-angular": "~0.901.4",

+ 2 - 2
src/app/src/app/app.component.html

@@ -1,8 +1,8 @@
 <mat-toolbar class="mat-elevation-z6">
   <mat-toolbar-row fxLayout="row" fxLayoutAlign="space-between center">
     <span><span [routerLink]="['/']" class="href"><img alt="logo" src="../assets/favicon-16x16.png">&nbsp;СВОБОДНЫЕ ДЕНЬГИ</span></span>
-    <div [ngSwitch]="hasAuth()">
-      <button *ngSwitchCase="true" mat-button>ВЫЙТИ</button>
+    <div [ngSwitch]="hasAuth() | async">
+      <button (click)="logout()" *ngSwitchCase="true" mat-button>ВЫЙТИ</button>
       <button *ngSwitchDefault [routerLink]="['/login']" mat-button>ВОЙТИ</button>
     </div>
   </mat-toolbar-row>

+ 13 - 2
src/app/src/app/app.component.ts

@@ -1,4 +1,8 @@
 import {Component} from '@angular/core';
+import {Observable} from "rxjs";
+import {PersonService} from "./person/person.service";
+import {map} from "rxjs/operators";
+import * as _ from 'underscore'
 
 @Component({
   selector: 'app-root',
@@ -7,8 +11,15 @@ import {Component} from '@angular/core';
 })
 export class AppComponent {
 
-  hasAuth(): boolean {
-    return false
+  constructor(private personService: PersonService) {
+
+  }
+
+  hasAuth(): Observable<boolean> {
+    return this.personService.getCurrentPerson().pipe(map(p => _.isObject(p.auth)))
   }
 
+  logout() {
+    this.personService.resetCurrentPerson()
+  }
 }

+ 2 - 2
src/app/src/app/app.login.component.html

@@ -1,7 +1,7 @@
 <div>
-  <p>Всё просто, для регистрации введите проверочную строку <strong>{{(qr | async).key}}</strong> или отсканируйте qr-код с помощью приложения Authenticator, затем введите шестизначный код из приложения и можно начинать.</p>
+  <p>Всё просто, для регистрации введите проверочную строку <strong>{{(qr | async)?.key}}</strong> или отсканируйте qr-код с помощью приложения Authenticator, затем введите шестизначный код из приложения и можно начинать.</p>
   <p>Или, если вы уже зарегистрированы, просто введите шестизначный код.</p>
-  <div><img alt="your qr-code here" src="{{(qr | async).img}}"></div>
+  <div><img alt="your qr-code here" src="{{(qr | async)?.img}}"></div>
   <div fxLayout="row" fxLayoutAlign="start center">
     <mat-form-field>
       <mat-label>Проверочный код</mat-label>

+ 2 - 0
src/app/src/app/app.login.component.ts

@@ -28,6 +28,7 @@ export class AppLoginComponent implements OnInit {
         this.personService.getCurrentPerson().subscribe(person => {
           this.httpClient.post("/api/new/person/check6", {siteId: person.siteId, code: c, key: qrRes.key}).subscribe((res: QrSixResponse) => {
             this.valid = res.ok ? 1 : 0;
+            if (res.ok) this.personService.setCurrentPerson(res.token)
           })
         })
       })
@@ -39,6 +40,7 @@ export class AppLoginComponent implements OnInit {
 export class QrSixResponse {
   public ok: boolean
   public register: boolean
+  public token: string
 }
 
 export class QrResponse {

+ 15 - 1
src/app/src/app/app.module.ts

@@ -19,9 +19,15 @@ import {MatInputModule} from "@angular/material/input";
 import {FormsModule} from "@angular/forms";
 import {MatIconModule} from "@angular/material/icon";
 import {PersonService} from "./person/person.service";
+import {JwtModule} from "@auth0/angular-jwt";
+import {NgxInitModule} from "ngx-init";
 
 registerLocaleData(localeRu);
 
+export function jwtTokenGetter() {
+  return localStorage.token
+}
+
 @NgModule({
   declarations: [
     AppComponent, AppLoginComponent, AppIndexComponent
@@ -39,7 +45,15 @@ registerLocaleData(localeRu);
     MatSnackBarModule,
     MatFormFieldModule,
     MatInputModule,
-    MatIconModule
+    MatIconModule,
+    NgxInitModule,
+    JwtModule.forRoot({
+      config: {
+        tokenGetter: jwtTokenGetter,
+        whitelistedDomains: ["127.0.0.1", "localhost"],
+        blacklistedRoutes: [],
+      },
+    }),
   ],
   providers: [
     PersonService,

+ 31 - 7
src/app/src/app/person/person.service.ts

@@ -1,11 +1,15 @@
 import {Injectable} from "@angular/core";
 import {Observable, ReplaySubject} from "rxjs";
 import {v4 as uuidv4} from 'uuid';
+import {JwtHelperService} from "@auth0/angular-jwt";
+import * as _ from 'underscore';
 
 @Injectable({providedIn: "root"})
 export class PersonService {
 
   private siteId: string
+  private helper = new JwtHelperService();
+  private currentPerson: Observable<Person>
 
   constructor() {
     if (!(this.siteId = localStorage.siteId)) {
@@ -13,14 +17,34 @@ export class PersonService {
     }
   }
 
+  resetCurrentPerson() {
+    localStorage.removeItem("token");
+    this.currentPerson = null;
+  }
+
+  setCurrentPerson(token: string) {
+    this.resetCurrentPerson();
+    localStorage.setItem("token", token);
+  }
+
   getCurrentPerson(): Observable<Person> {
-    let ret = new ReplaySubject<Person>();
-    setTimeout(() => {
-      let p = new Person();
-      p.siteId = this.siteId
-      ret.next(p)
-    })
-    return ret;
+    if (this.currentPerson == null) {
+      let ret = this.currentPerson = new ReplaySubject<Person>();
+      setTimeout(() => {
+        let p = new Person();
+        p.siteId = this.siteId
+        let tkr = localStorage.getItem("token");
+        if (!_.isEmpty(tkr)) {
+          let decodedToken = this.helper.decodeToken(tkr);
+          //const expirationDate = this.helper.getTokenExpirationDate(tkr);
+          if (!this.helper.isTokenExpired(tkr)) {
+            p.auth = {};
+          }
+        }
+        ret.next(p)
+      }, 100)
+    }
+    return this.currentPerson;
   }
 
 }

+ 24 - 0
src/app/yarn.lock

@@ -223,6 +223,13 @@
   resolved "https://registry.yarnpkg.com/@angular/router/-/router-9.1.5.tgz#eafa8a1a2a015498fb39fdc789ad723f4e743b2b"
   integrity sha512-tbkev20+W4euV9FCjHc5vKKpa9/rFDT3EMpsDap9cOVryv84haYAh/X4mrc3+rmpJM9nc7Nh6nuaEcgIcjK9xQ==
 
+"@auth0/angular-jwt@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@auth0/angular-jwt/-/angular-jwt-4.0.0.tgz#1930088bcf6ad9e2ff26f34196cec6ebf3853c15"
+  integrity sha512-CHvk1zJ9jpQupl0f5y7EmTvYAwugyFvC4ztLsZKr7ZC7anNVaDd1+pDFJYS+ZEU9jLWzE74+AfVKfigImADJuw==
+  dependencies:
+    url "^0.11.0"
+
 "@babel/code-frame@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
@@ -4806,6 +4813,11 @@ mkdirp@^1.0.3:
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
   integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
 
+moment@^2.25.3:
+  version "2.25.3"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.25.3.tgz#252ff41319cf41e47761a1a88cab30edfe9808c0"
+  integrity sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg==
+
 move-concurrently@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -4883,6 +4895,13 @@ neo-async@^2.5.0, neo-async@^2.6.1:
   resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
   integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
 
+ngx-init@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/ngx-init/-/ngx-init-0.1.1.tgz#0952a95fc336a13e8da2988bedfc7312769e1f2e"
+  integrity sha512-VnY4uUdA63EjdcSi3cPJXRBYU8MTg07S8T2nPVj5F7KpRtT5hiKz9Ls6necZ4owcTwZbGQDvV28axXgr3w9f/Q==
+  dependencies:
+    tslib "^1.9.0"
+
 nice-try@^1.0.4:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -7376,6 +7395,11 @@ typescript@~3.8.3:
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
   integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
 
+underscore@^1.10.2:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.10.2.tgz#73d6aa3668f3188e4adb0f1943bd12cfd7efaaaf"
+  integrity sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==
+
 unicode-canonical-property-names-ecmascript@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"

+ 1 - 1
src/main/kotlin/inn/ocsf/bee/freigeld/core/model/PersonIdentityTypes.kt

@@ -9,7 +9,7 @@ data class PersonIdentityFullName(val fullName: String) : PersonIdentity {
     }
 }
 
-data class PersonIdentitySecret(val secret: String) : PersonIdentity {
+data class PersonIdentitySecret(val secret: String, val codes: List<String>?) : PersonIdentity {
 
     override fun getKey(): String {
         return secret

+ 225 - 0
src/main/kotlin/inn/ocsf/bee/freigeld/serve/Security.kt

@@ -0,0 +1,225 @@
+package inn.ocsf.bee.freigeld.serve
+
+import io.jsonwebtoken.Claims
+import io.jsonwebtoken.ExpiredJwtException
+import io.jsonwebtoken.Jwts
+import io.jsonwebtoken.security.Keys
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.authentication.AuthenticationManager
+import org.springframework.security.authentication.BadCredentialsException
+import org.springframework.security.authentication.DisabledException
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
+import org.springframework.security.config.http.SessionCreationPolicy
+import org.springframework.security.core.AuthenticationException
+import org.springframework.security.core.GrantedAuthority
+import org.springframework.security.core.authority.SimpleGrantedAuthority
+import org.springframework.security.core.context.SecurityContextHolder
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.core.userdetails.UserDetails
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.crypto.password.NoOpPasswordEncoder
+import org.springframework.security.crypto.password.PasswordEncoder
+import org.springframework.security.web.AuthenticationEntryPoint
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
+import org.springframework.stereotype.Component
+import org.springframework.web.filter.OncePerRequestFilter
+import java.io.Serializable
+import java.nio.charset.StandardCharsets
+import java.util.*
+import javax.inject.Inject
+import javax.servlet.FilterChain
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+import kotlin.collections.HashMap
+
+
+@Configuration
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+class Security : WebSecurityConfigurerAdapter() {
+
+    @Inject
+    private lateinit var jwtUserDetailsService: JwtUserDetailsService
+
+    @Inject
+    private lateinit var jwtAuthEntryPoint: JwtAuthEntryPoint
+
+    @Inject
+    private lateinit var jwtRequestFilter: JwtRequestFilter
+
+    @Bean
+    @Throws(Exception::class)
+    override fun authenticationManagerBean(): AuthenticationManager? {
+        return super.authenticationManagerBean()
+    }
+
+
+    @Autowired
+    @Throws(Exception::class)
+    fun configureGlobal(auth: AuthenticationManagerBuilder) {
+        auth.userDetailsService<UserDetailsService>(jwtUserDetailsService).passwordEncoder(passwordEncoder())
+    }
+
+
+    @Bean
+    fun passwordEncoder(): PasswordEncoder {
+        return NoOpPasswordEncoder.getInstance()
+    }
+
+    @Throws(Exception::class)
+    override fun configure(httpSecurity: HttpSecurity) {
+        httpSecurity
+                .csrf()
+                .disable()
+                .authorizeRequests()
+                .antMatchers("/api/new/person/**").permitAll()
+                .anyRequest().authenticated()
+                .and().exceptionHandling()
+                .authenticationEntryPoint(jwtAuthEntryPoint)
+                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+
+        httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter::class.java)
+    }
+}
+
+@Component
+class JwtAuthEntryPoint : AuthenticationEntryPoint, Serializable {
+
+    override fun commence(p0: HttpServletRequest?, p1: HttpServletResponse?, p2: AuthenticationException?) {
+        p1?.sendError(HttpServletResponse.SC_UNAUTHORIZED, "unauthorized")
+    }
+
+}
+
+@Component
+class JwtTokenUtil : Serializable {
+
+    companion object {
+        val secret = Keys.hmacShaKeyFor("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla vel fermentum eros, a molestie nisl. Nunc viverra ante nulla, non tincidunt leo rutrum vel. Donec pulvinar ornare eros, at bibendum augue efficitur eu. Nunc sit amet massa sit amet nisi fringilla commodo ac in mi. Morbi viverra enim vel rhoncus semper. Phasellus semper convallis eros, ac pretium nulla aliquet ut. Ut fringilla leo ut purus gravida, quis vestibulum mi commodo. Curabitur feugiat odio at dapibus aliquam. Phasellus eget eros quis enim vulputate fringilla a sit amet nibh. In eget metus sed dui maximus auctor. Nunc eu ornare ligula, bibendum luctus lectus.".toByteArray(StandardCharsets.UTF_8))
+
+        val tokenLifetime = 5 * 60 * 60L
+    }
+
+    fun getUsernameFromToken(token: String?): String {
+        return getClaimFromToken(token, Claims::getSubject)
+    }
+
+    fun getExpirationDateFromToken(token: String?): Date {
+        return getClaimFromToken<Date>(token, Claims::getExpiration)
+    }
+
+    fun <T> getClaimFromToken(token: String?, claimsResolver: (Claims) -> T): T {
+        val claims: Claims = getAllClaimsFromToken(token)
+        return claimsResolver(claims)
+    }
+
+    private fun getAllClaimsFromToken(token: String?): Claims {
+        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).body
+    }
+
+    private fun isTokenExpired(token: String?): Boolean {
+        val expiration: Date = getExpirationDateFromToken(token)
+        return expiration.before(Date())
+    }
+
+    fun generateToken(userDetails: UserDetails): String {
+        val claims: Map<String, Any> = HashMap()
+        return doGenerateToken(claims, userDetails.username)
+    }
+
+    private fun doGenerateToken(claims: Map<String, Any>, subject: String): String {
+        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(Date(System.currentTimeMillis()))
+                .setExpiration(Date(System.currentTimeMillis() + tokenLifetime * 1000))
+                .signWith(secret).compact()
+    }
+
+    fun validateToken(token: String?, userDetails: UserDetails): Boolean {
+        val username = getUsernameFromToken(token)
+        return username == userDetails.username && !isTokenExpired(token)
+    }
+}
+
+@Component
+class JwtRequestFilter : OncePerRequestFilter() {
+
+    @Inject
+    private lateinit var jwtTokenUtil: JwtTokenUtil
+
+    @Inject
+    private lateinit var jwtUserDetailsService: JwtUserDetailsService
+
+    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, next: FilterChain) {
+        val requestTokenHeader = request.getHeader("Authorization")
+        var username: String? = null
+        var jwtToken: String? = null
+
+        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
+            jwtToken = requestTokenHeader.substring(7)
+            try {
+                username = jwtTokenUtil.getUsernameFromToken(jwtToken)
+            } catch (e: IllegalArgumentException) {
+                System.out.println("Unable to get JWT Token")
+            } catch (e: ExpiredJwtException) {
+                System.out.println("JWT Token has expired")
+            }
+        } else {
+            logger.warn("JWT Token does not begin with Bearer String")
+        }
+        if (username != null && SecurityContextHolder.getContext().authentication == null) {
+            val userDetails = jwtUserDetailsService.loadUserByUsername(username)
+            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
+                val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(
+                        userDetails, null, userDetails.authorities)
+                usernamePasswordAuthenticationToken.details = WebAuthenticationDetailsSource().buildDetails(request)
+                SecurityContextHolder.getContext().authentication = usernamePasswordAuthenticationToken
+            }
+        }
+        next.doFilter(request, response)
+    }
+}
+
+abstract class JwtAuthenticationController {
+
+    @Autowired
+    private lateinit var authenticationManager: AuthenticationManager
+
+    @Autowired
+    private lateinit var jwtTokenUtil: JwtTokenUtil
+
+    @Autowired
+    private lateinit var userDetailsService: JwtUserDetailsService
+
+    fun createAuthenticationToken(username: String): String {
+        authenticate(username)
+        val userDetails: UserDetails = userDetailsService.loadUserByUsername(username)
+        return jwtTokenUtil.generateToken(userDetails)
+    }
+
+    @Throws(Exception::class)
+    private fun authenticate(username: String) {
+        try {
+            authenticationManager.authenticate(UsernamePasswordAuthenticationToken(username, ""))
+        } catch (e: DisabledException) {
+            throw Exception("USER_DISABLED", e)
+        } catch (e: BadCredentialsException) {
+            throw Exception("INVALID_CREDENTIALS", e)
+        }
+    }
+}
+
+@Component
+class JwtUserDetailsService : UserDetailsService {
+
+    override fun loadUserByUsername(username: String): UserDetails {
+        return JwtUser(username, "", mutableListOf(SimpleGrantedAuthority("PERSON")))
+    }
+
+}
+
+class JwtUser(username: String, password: String, authorities: MutableCollection<out GrantedAuthority>) : User(username, password, authorities)

+ 11 - 11
src/main/kotlin/inn/ocsf/bee/freigeld/serve/rest/PersonController.kt

@@ -7,6 +7,7 @@ import dev.samstevens.totp.code.DefaultCodeVerifier
 import dev.samstevens.totp.code.HashingAlgorithm
 import dev.samstevens.totp.qr.QrData
 import dev.samstevens.totp.qr.ZxingPngQrGenerator
+import dev.samstevens.totp.recovery.RecoveryCodeGenerator
 import dev.samstevens.totp.secret.DefaultSecretGenerator
 import dev.samstevens.totp.time.SystemTimeProvider
 import dev.samstevens.totp.time.TimeProvider
@@ -16,6 +17,7 @@ import inn.ocsf.bee.freigeld.core.model.PersonIdentityFullName
 import inn.ocsf.bee.freigeld.core.model.PersonIdentitySecret
 import inn.ocsf.bee.freigeld.core.model.data.PersonData
 import inn.ocsf.bee.freigeld.core.repo.PersonRepository
+import inn.ocsf.bee.freigeld.serve.JwtAuthenticationController
 import inn.ocsf.bee.freigeld.utils.KeyValueBucket.personToSite
 import inn.ocsf.bee.freigeld.utils.KeyValueBucket.secretToSite
 import inn.ocsf.bee.freigeld.utils.KeyValueStorage
@@ -25,9 +27,9 @@ import org.springframework.web.bind.annotation.*
 import java.util.*
 import javax.inject.Inject
 
-
 @RestController
-class PersonController {
+@CrossOrigin
+class PersonController : JwtAuthenticationController() {
 
     @Inject
     private lateinit var storage: KeyValueStorage
@@ -45,17 +47,13 @@ class PersonController {
 
         storage.put(secretToSite, secret, siteId)
 
-        val data = QrData.Builder().secret(secret).algorithm(HashingAlgorithm.SHA1).label("personal wallet").issuer("FreiGeld").digits(6).period(30).build()
+        val data = QrData.Builder().secret(secret).algorithm(HashingAlgorithm.SHA1).label("personal kalita").issuer("FreiGeld").digits(6).period(30).build()
 
         val generator = ZxingPngQrGenerator()
         val imageData = generator.generate(data)
         val mimeType = generator.imageMimeType
         val dataUri = getDataUriForImage(imageData, mimeType)
 
-        /*
-        val recoveryCodeGenerator = RecoveryCodeGenerator()
-        val codes = recoveryCodeGenerator.generateCodes(16)
-        */
         return ResponseEntity.ok(QrDataResponse(secret, dataUri))
     }
 
@@ -73,7 +71,9 @@ class PersonController {
             val person = PersonData.NaturalPersonImpl("${Nomen.randomName()}_${RandomStringUtils.randomAlphanumeric(5)}")
             world.addPerson(person)
             world.setPersonIdentity(person, PersonIdentityFullName(person.fullName))
-            world.setPersonIdentity(person, PersonIdentitySecret(req.key))
+            val recoveryCodeGenerator = RecoveryCodeGenerator()
+            val codes = recoveryCodeGenerator.generateCodes(16)
+            world.setPersonIdentity(person, PersonIdentitySecret(req.key, codes.toList()))
             storage.put(personToSite, person.id, req.siteId)
             register = true
             person
@@ -83,8 +83,8 @@ class PersonController {
                 verifier.isValidCode(secret.key, req.code)
             }.map { it.person }.firstOrNull()
         }
-
-        return ResponseEntity.ok(QrSixResponse(thisPerson != null, register))
+        val token = thisPerson?.let { createAuthenticationToken(it.id.toString()) }
+        return ResponseEntity.ok(QrSixResponse(thisPerson != null, register, token))
     }
 
 }
@@ -93,4 +93,4 @@ data class QrDataResponse(val key: String, val img: String)
 
 data class QrSixRequest(val siteId: UUID, val code: String, val key: String)
 
-data class QrSixResponse(val ok: Boolean, val register: Boolean)
+data class QrSixResponse(val ok: Boolean, val register: Boolean, val token: String?)