working backend changes to generate JWT tokens during signin and signup. (#9)

This commit is contained in:
Jason Kulatunga 2022-11-02 00:12:54 -07:00 committed by GitHub
parent 9d56fa2896
commit 032946100c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 337 additions and 178 deletions

View File

@ -220,3 +220,15 @@ a CDN or minimal Nginx deployment.
- FASTEN_ISSUER_JWT_KEY
- FASTEN_COUCHDB_ADMIN_USERNAME
- FASTEN_COUCHDB_ADMIN_PASSWORD
### Generate JWT for local use
```bash
curl -X POST http://localhost:9090/api/auth/signup -H 'Content-Type: application/json' -d '{"username":"user1","password":"user1"}'
curl -X POST http://localhost:9090/api/auth/signin -H 'Content-Type: application/json' -d '{"username":"user1","password":"user1"}'
curl -H "Authorization: Bearer ${JWT_TOKEN_HERE}" http://localhost:5984/_session
```

View File

@ -32,6 +32,8 @@ func (c *configuration) Init() error {
c.SetDefault("couchdb.admin.username", "admin")
c.SetDefault("couchdb.admin.password", "mysecretpassword")
c.SetDefault("jwt.issuer.key", "thisismysupersecuressessionsecretlength")
c.SetDefault("log.level", "INFO")
c.SetDefault("log.file", "")

View File

@ -9,6 +9,7 @@ import (
_ "github.com/go-kivik/couchdb/v3" // The CouchDB driver
"github.com/go-kivik/kivik/v3"
"github.com/sirupsen/logrus"
"log"
)
func NewRepository(appConfig config.Interface, globalLogger logrus.FieldLogger) (DatabaseRepository, error) {
@ -69,9 +70,36 @@ func (cr *couchdbRepository) CreateUser(ctx context.Context, user *models.User)
}
db := cr.client.DB(ctx, "_users")
_, err := db.Put(ctx, newUser.ID, newUser)
if err != nil {
return err
}
//TODO: we should create an index for this database now
//db.CreateIndex(ctx, )
return err
}
func (cr *couchdbRepository) VerifyUser(ctx context.Context, user *models.User) error {
couchdbUrl := fmt.Sprintf("%s://%s:%s", cr.appConfig.GetString("couchdb.scheme"), cr.appConfig.GetString("couchdb.host"), cr.appConfig.GetString("couchdb.port"))
userDatabase, err := kivik.New("couch", couchdbUrl)
if err != nil {
return fmt.Errorf("Failed to connect to database! - %v", err)
}
err = userDatabase.Authenticate(context.Background(),
couchdb.BasicAuth(user.Username, user.Password),
)
session, err := userDatabase.Session(context.Background())
if err != nil {
return err
}
log.Printf("SESSION INFO: %v", session)
//TODO: return session info
return nil
}
func (cr *couchdbRepository) Close() error {
return nil
}

View File

@ -10,4 +10,5 @@ type DatabaseRepository interface {
Close() error
CreateUser(context.Context, *models.User) error
VerifyUser(context.Context, *models.User) error
}

View File

@ -1,10 +1,14 @@
package handler
import (
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/database"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
"github.com/gin-gonic/gin"
jwt "github.com/golang-jwt/jwt/v4"
"log"
"net/http"
"time"
)
func AuthSignup(c *gin.Context) {
@ -23,3 +27,45 @@ func AuthSignup(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true})
}
func AuthSignin(c *gin.Context) {
databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository)
appConfig := c.MustGet("CONFIG").(config.Interface)
var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
err := databaseRepo.VerifyUser(c, &user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
//TODO: we can derive the encryption key and the hash'ed user from the responseData sub. For now the Sub will be the user id prepended with hello.
userFastenToken, err := jwtGenerateFastenTokenFromUser(user, appConfig.GetString("jwt.issuer.key"))
c.JSON(http.StatusOK, gin.H{"success": true, "data": userFastenToken})
}
func jwtGenerateFastenTokenFromUser(user models.User, issuerSigningKey string) (string, error) {
log.Printf("ISSUER KEY: " + issuerSigningKey)
userClaims := jwt.RegisteredClaims{
// In JWT, the expiry time is expressed as unix milliseconds
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "docker-fastenhealth",
Subject: user.Username,
}
//FASTEN_JWT_ISSUER_KEY
token := jwt.NewWithClaims(jwt.SigningMethodHS256, userClaims)
//token.Header["kid"] = "docker"
tokenString, err := token.SignedString([]byte(issuerSigningKey))
if err != nil {
return "", err
}
return tokenString, nil
}

View File

@ -41,6 +41,7 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
})
api.POST("/auth/signup", handler.AuthSignup)
api.POST("/auth/signin", handler.AuthSignin)
r.Any("/database/*proxyPath", handler.CouchDBProxy)
r.GET("/cors/*proxyPath", handler.CORSProxy)

View File

@ -6,7 +6,7 @@ FROM couchdb:3.2
ENV FASTEN_COUCHDB_ADMIN_USERNAME=admin
ENV FASTEN_COUCHDB_ADMIN_PASSWORD=mysecretpassword
ENV FASTEN_ISSUER_JWT_KEY=mysessionpassword
ENV FASTEN_JWT_ISSUER_KEY=thisismysupersecuressessionsecretlength
ARG S6_ARCH=amd64
RUN curl https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${S6_ARCH}.tar.gz -L -s --output /tmp/s6-overlay-${S6_ARCH}.tar.gz \

View File

@ -3,11 +3,11 @@
if [ -f "/opt/couchdb/data/.config_complete" ]; then
echo "Couchdb config has already completed, skipping"
else
FASTEN_ISSUER_JWT_KEY_BASE64=$(echo "${FASTEN_ISSUER_JWT_KEY}" | base64)
#echo -n means we dont pass newline to base64 (ugh). eg. hello = aGVsbG8K vs aGVsbG8=
FASTEN_JWT_ISSUER_KEY_BASE64=$(echo -n "${FASTEN_JWT_ISSUER_KEY}" | base64)
cat << EOF >> /opt/couchdb/etc/local.ini
cat << EOF >> /opt/couchdb/etc/local.d/generated.ini
; ------------------------------------------ GENERATED MODIFICATIONS
; ------------------------------------------ GENERATED MODIFICATIONS
@ -17,12 +17,13 @@ cat << EOF >> /opt/couchdb/etc/local.ini
required_claims = exp, {iss, "docker-fastenhealth"}
[jwt_keys]
hmac:_default = ${FASTEN_ISSUER_JWT_KEY_BASE64}
hmac:_default = ${FASTEN_JWT_ISSUER_KEY_BASE64}
; users should change this default password
[admins]
${FASTEN_COUCHDB_ADMIN_USERNAME} = ${FASTEN_COUCHDB_ADMIN_PASSWORD}
EOF
# create the config complete flag

View File

@ -36,6 +36,7 @@
"garbados-crypt": "^3.0.0-beta",
"humanize-duration": "^3.27.3",
"idb": "^7.1.0",
"jose": "^4.10.4",
"moment": "^2.29.4",
"ng2-charts": "^2.3.0",
"ngx-dropzone": "^3.1.0",

View File

@ -27,6 +27,7 @@ import { HighlightModule, HIGHLIGHT_OPTIONS } from 'ngx-highlightjs';
import {AuthInterceptorService} from './services/auth-interceptor.service';
import { MomentModule } from 'ngx-moment';
import { EncryptionManagerComponent } from './pages/encryption-manager/encryption-manager.component';
import {AuthService} from './services/auth.service';
@NgModule({
declarations: [
@ -59,7 +60,7 @@ import { EncryptionManagerComponent } from './pages/encryption-manager/encryptio
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptorService,
multi: true,
deps: [FastenDbService, Router]
deps: [AuthService, Router]
},
IsAuthenticatedAuthGuard,
EncryptionEnabledAuthGuard,

View File

@ -1,16 +1,16 @@
import { Injectable } from '@angular/core';
import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router} from '@angular/router';
import { FastenDbService } from '../services/fasten-db.service';
import {AuthService} from '../services/auth.service';
@Injectable()
export class IsAuthenticatedAuthGuard implements CanActivate {
constructor(private fastenDbService: FastenDbService, private router: Router) {
constructor(private authService: AuthService, private router: Router) {
}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise <boolean> {
//check if the user is authenticated, if not, redirect to login
if (! await this.fastenDbService.IsAuthenticated()) {
if (! await this.authService.IsAuthenticated()) {
return await this.router.navigate(['/auth/signin']);
}
// continue as normal

View File

@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core';
import {FastenDbService} from '../../services/fasten-db.service';
import { Router } from '@angular/router';
import {AuthService} from '../../services/auth.service';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
@ -8,10 +9,10 @@ import { Router } from '@angular/router';
})
export class HeaderComponent implements OnInit {
current_user: string
constructor(private fastenDb: FastenDbService, private router: Router) { }
constructor(private authService: AuthService, private router: Router) { }
ngOnInit() {
this.current_user = this.fastenDb.current_user
this.current_user = this.authService.GetCurrentUser()
}
closeMenu(e) {
@ -25,7 +26,7 @@ export class HeaderComponent implements OnInit {
}
signOut(e) {
this.fastenDb.Logout()
this.authService.Logout()
.then(() => this.router.navigate(['auth/signin']))
}
}

View File

@ -0,0 +1,15 @@
export interface Session {
ok: boolean;
userCtx?: UserCtx;
info?: Info;
}
export interface UserCtx {
name?: any;
roles?: any[];
}
export interface Info {
authenticated?: string
authentication_handlers: string[];
}

View File

@ -3,6 +3,7 @@ import {Source} from '../../../lib/models/database/source';
export class SourceSyncMessage {
source: Source
current_user: string
auth_token: string
couchdb_endpoint_base: string
fasten_api_endpoint_base: string
response?: any

View File

@ -37,7 +37,14 @@ export class AuthSigninComponent implements OnInit {
let state = params.get('state') // eyJhbGciOiJSUzI1...rest_of_ID_Token
this.resetUrlOnCallback()
this.authService.IdpCallback(idpType, state, code).then(console.log)
this.authService.IdpCallback(idpType, state, code)
.then(() => this.router.navigateByUrl('/dashboard'))
.catch((err)=>{
const toastNotification = new ToastNotification()
toastNotification.type = ToastType.Error
toastNotification.message = "an error occurred while signing in"
this.toastService.show(toastNotification)
})
}
}
@ -45,7 +52,7 @@ export class AuthSigninComponent implements OnInit {
signinSubmit(){
this.submitted = true;
this.fastenDb.Signin(this.existingUser.username, this.existingUser.password)
this.authService.Signin(this.existingUser.username, this.existingUser.password)
.then(() => this.router.navigateByUrl('/dashboard'))
.catch((err)=>{
if(err?.name){

View File

@ -4,6 +4,7 @@ import {User} from '../../../lib/models/fasten/user';
import {Router} from '@angular/router';
import {ToastNotification, ToastType} from '../../models/fasten/toast';
import {ToastService} from '../../services/toast.service';
import {AuthService} from '../../services/auth.service';
@Component({
selector: 'app-auth-signup',
@ -16,7 +17,7 @@ export class AuthSignupComponent implements OnInit {
errorMsg: string = ""
constructor(
private fastenDb: FastenDbService,
private authService: AuthService,
private router: Router,
private toastService: ToastService
) { }
@ -27,7 +28,7 @@ export class AuthSignupComponent implements OnInit {
signupSubmit(){
this.submitted = true;
this.fastenDb.Signup(this.newUser).then((tokenResp: any) => {
this.authService.Signup(this.newUser).then((tokenResp: any) => {
console.log(tokenResp);
this.router.navigateByUrl('/dashboard');
},

View File

@ -4,6 +4,9 @@ import { FastenDbService } from './fasten-db.service';
import {Router} from '@angular/router';
import {Observable, of, throwError} from 'rxjs';
import {catchError} from 'rxjs/operators';
import {AuthService} from './auth.service';
import {GetEndpointAbsolutePath} from '../../lib/utils/endpoint_absolute_path';
import {environment} from '../../environments/environment';
@Injectable({
providedIn: 'root'
@ -12,13 +15,13 @@ import {catchError} from 'rxjs/operators';
// based on https://stackoverflow.com/questions/46017245/how-to-handle-unauthorized-requestsstatus-with-401-or-403-with-new-httpclient
export class AuthInterceptorService implements HttpInterceptor {
constructor(private fastenDbService: FastenDbService, private router: Router) { }
constructor(private authService: AuthService, private router: Router) { }
private handleAuthError(err: HttpErrorResponse): Observable<any> {
//handle your auth error or rethrow
if (err.status === 401 || err.status === 403) {
//navigate /delete cookies or whatever
this.fastenDbService.Logout()
this.authService.Logout()
this.router.navigateByUrl(`/auth/signin`);
// if you've caught / handled the error, you don't want to rethrow it unless you also want downstream consumers to have to handle it as well.
return of(err.message); // or EMPTY may be appropriate here
@ -27,13 +30,40 @@ export class AuthInterceptorService implements HttpInterceptor {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
//only intercept requests to the fasten API, all other requests should be sent as-is
if(!req.url.startsWith('/api/secure/')){
let authToken = this.authService.GetAuthToken()
if(!authToken){
//no authToken available, lets just handle the request as-is
return next.handle(req)
}
// catch the error, make specific functions for catching specific errors and you can chain through them with more catch operators
return next.handle(req).pipe(catchError(x=> this.handleAuthError(x))); //here use an arrow function, otherwise you may get "Cannot read property 'navigate' of undefined" on angular 4.4.2/net core 2/webpack 2.70
//only intercept requests to the Fasten API, Database & Lighthouse, all other requests should be sent as-is
let reqUrl = new URL(req.url)
let lighthouseUrl = new URL(GetEndpointAbsolutePath(globalThis.location, environment.lighthouse_api_endpoint_base))
let apiUrl = new URL(GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base))
//skip database, header is sent automatically via PouchDB
// let databaseUrl = new URL(GetEndpointAbsolutePath(globalThis.location, environment.couchdb_endpoint_base))
if(
(reqUrl.origin == lighthouseUrl.origin && reqUrl.pathname.startsWith(lighthouseUrl.pathname))
){
//all requests to the lighthouse require the JWT
console.log("making authorized request...")
// Clone the request to add the new auth header.
const authReq = req.clone({headers: req.headers.set('Authorization', 'Bearer ' + this.authService.GetAuthToken())});
// catch the error, make specific functions for catching specific errors and you can chain through them with more catch operators
return next.handle(authReq).pipe(catchError(x=> this.handleAuthError(x))); //here use an arrow function, otherwise you may get "Cannot read property 'navigate' of undefined" on angular 4.4.2/net core 2/webpack 2.70
}
// else if(){
// //TODO: only CORS requests to the API endpoint require JWT, but they also require a custom header.
//
// //(reqUrl.origin == lighthouseUrl.origin && reqUrl.pathname.startsWith(lighthouseUrl.pathname)) ||
// // () ||
// // (reqUrl.origin == apiUrl.origin && reqUrl.pathname.startsWith(apiUrl.pathname))
//
// }
return next.handle(req)
}
}

View File

@ -1,12 +1,15 @@
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
import {HttpClientTestingModule} from '@angular/common/http/testing';
describe('AuthService', () => {
let service: AuthService;
beforeEach(() => {
TestBed.configureTestingModule({});
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
});
service = TestBed.inject(AuthService);
});

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {FastenDbService} from './fasten-db.service';
import {User} from '../../lib/models/fasten/user';
import {environment} from '../../environments/environment';
@ -7,16 +7,20 @@ import {GetEndpointAbsolutePath} from '../../lib/utils/endpoint_absolute_path';
import {ResponseWrapper} from '../models/response-wrapper';
import * as Oauth from '@panva/oauth4webapi';
import {SourceState} from '../models/fasten/source-state';
import {Session} from '../models/database/session';
import * as jose from 'jose';
@Injectable({
providedIn: 'root'
})
export class AuthService {
FASTEN_JWT_LOCALSTORAGE_KEY = 'token';
constructor(private _httpClient: HttpClient) {
}
//Third-party JWT auth, used by Fasten Cloud
public async IdpConnect(idp_type: string) {
const state = this.uuidV4()
@ -92,11 +96,98 @@ export class AuthService {
let fastenApiEndpointBase = GetEndpointAbsolutePath(globalThis.location,environment.fasten_api_endpoint_base)
let resp = await this._httpClient.post<ResponseWrapper>(`${fastenApiEndpointBase}/auth/callback/${idp_type}`, payload).toPromise()
this.setAuthToken(resp.data)
return resp
}
//Primary auth used by self-hosted Fasten
/**
* Signup (and Signin) both require an "online" user.
* @param newUser
* @constructor
*/
public async Signup(newUser?: User): Promise<any> {
let fastenApiEndpointBase = GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)
let resp = await this._httpClient.post<ResponseWrapper>(`${fastenApiEndpointBase}/auth/signup`, newUser).toPromise()
console.log(resp)
this.setAuthToken(resp.data)
}
public async Signin(username: string, pass: string): Promise<any> {
let currentUser = new User()
currentUser.username = username
currentUser.password = pass
let fastenApiEndpointBase = GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)
let resp = await this._httpClient.post<ResponseWrapper>(`${fastenApiEndpointBase}/auth/signin`, currentUser).toPromise()
this.setAuthToken(resp.data)
}
//TODO: now that we've moved to remote-first database, we can refactor and simplify this function significantly.
public async IsAuthenticated(): Promise<boolean> {
let authToken = this.GetAuthToken()
let hasAuthToken = !!authToken
if(!hasAuthToken){
return false
}
//check if the authToken works
let databaseEndpointBase = GetEndpointAbsolutePath(globalThis.location, environment.couchdb_endpoint_base)
try {
let resp = await this._httpClient.get<any>(`${databaseEndpointBase}/_session`, {
headers: new HttpHeaders({
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`
})
}).toPromise()
// logic to check if user is logged in here.
let session = resp as Session
if(!session.ok || session?.info?.authenticated != "jwt" || !session.userCtx?.name){
//invalid session, not jwt auth, or username is empty
return false
}
return true
} catch (e) {
return false
}
}
public GetAuthToken(): string {
return localStorage.getItem(this.FASTEN_JWT_LOCALSTORAGE_KEY);
}
public GetCurrentUser(): string {
let authToken = this.GetAuthToken()
if(!authToken){
throw new Error("no auth token found")
}
//parse the authToken to get user information
let jwtClaims = jose.decodeJwt(authToken)
return jwtClaims.sub
}
public async Logout(): Promise<any> {
return localStorage.removeItem(this.FASTEN_JWT_LOCALSTORAGE_KEY)
// // let remotePouchDb = new PouchDB(this.getRemoteUserDb(localStorage.getItem("current_user")), {skip_setup: true});
// if(this.pouchDb){
// await this.pouchDb.logOut()
// }
// await this.Close()
}
/////////////////////////////////////////////////////////////////////////////////////////////////
//Private Methods
/////////////////////////////////////////////////////////////////////////////////////////////////
private setAuthToken(token: string) {
localStorage.setItem(this.FASTEN_JWT_LOCALSTORAGE_KEY, token)
}
private uuidV4(){
// @ts-ignore
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>

View File

@ -7,7 +7,7 @@ import {Summary} from '../../lib/models/fasten/summary';
import {DocType} from '../../lib/database/constants';
import {ResourceTypeCounts} from '../../lib/models/fasten/source-summary';
import {Base64} from '../../lib/utils/base64';
import * as jose from 'jose'
// PouchDB & plugins (must be similar to the plugins specified in pouchdb repository)
import * as PouchDB from 'pouchdb/dist/pouchdb';
@ -21,6 +21,7 @@ import PouchAuth from 'pouchdb-authentication'
import {PouchdbCrypto} from '../../lib/database/plugins/crypto';
import {environment} from '../../environments/environment';
import {GetEndpointAbsolutePath} from '../../lib/utils/endpoint_absolute_path';
import {AuthService} from './auth.service';
PouchDB.plugin(PouchAuth);
@Injectable({
@ -28,65 +29,25 @@ PouchDB.plugin(PouchAuth);
})
export class FastenDbService extends PouchdbRepository {
constructor(private _httpClient: HttpClient) {
// There are 3 different ways to initialize the Database
// - explicitly after signin/signup
// - explicitly during web-worker init (not supported by this class, see PouchdbRepository.NewPouchdbRepositoryWebWorker)
// - implicitly after Lighthouse redirect (when user is directed back to the app)
// Three peices of information are required during intialization
// - couchdb endpoint (constant, see environment.couchdb_endpoint_base)
// - username
// - JWT token
constructor(private _httpClient: HttpClient, private authService: AuthService) {
super(environment.couchdb_endpoint_base);
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// Auth methods
/**
* Signin (and Signup) both require an "online" user.
* @param username
* @param pass
* @constructor
*/
public async Signin(username: string, pass: string): Promise<any> {
let remotePouchDb = new PouchDB(this.getRemoteUserDb(username), {skip_setup: true});
return await remotePouchDb.logIn(username, pass)
.then((loginResp)=>{
this.current_user = loginResp.name
return this.postLoginHook(loginResp.name, remotePouchDb)
})
.catch((err) => {
console.error("an error occurred during login/setup", err)
throw err
})
}
/**
* Signup (and Signin) both require an "online" user.
* @param newUser
* @constructor
*/
public async Signup(newUser?: User): Promise<any> {
console.log("STARTING SIGNUP")
let fastenApiEndpointBase = environment.fasten_api_endpoint_base
if (!(fastenApiEndpointBase.indexOf('http://') === 0 || fastenApiEndpointBase.indexOf('https://') === 0)){
//relative, we need to retrieve the absolutePath from base
fastenApiEndpointBase = GetEndpointAbsolutePath(globalThis.location,fastenApiEndpointBase)
}
let resp = await this._httpClient.post<ResponseWrapper>(`${fastenApiEndpointBase}/auth/signup`, newUser).toPromise()
console.log(resp)
return this.Signin(newUser.username, newUser.password);
}
public async Logout(): Promise<any> {
// let remotePouchDb = new PouchDB(this.getRemoteUserDb(localStorage.getItem("current_user")), {skip_setup: true});
if(this.pouchDb){
await this.pouchDb.logOut()
}
await this.Close()
}
/**
* Try to get PouchDB database using session information
* Try to get PouchDB database using token auth information
* This method must handle 2 types of authentication
* - pouchdb init after signin/signup
* - implicit init after lighthouse redirect
* @constructor
*/
public override async GetSessionDB(): Promise<PouchDB.Database> {
@ -95,39 +56,24 @@ export class FastenDbService extends PouchdbRepository {
return this.pouchDb
}
//Since we dont have a pre-configured pouchDB already, see if we have an active session to the remote database.
let sessionDb = new PouchDB(this.getRemoteUserDb("placeholder"))
const session = await sessionDb.getSession()
console.log("Session found...", session)
const authUser = session?.userCtx?.name
if(authUser){
this.pouchDb = new PouchDB(this.getRemoteUserDb(authUser))
this.current_user = authUser
//check if we have a JWT token (we should, otherwise the auth-guard would have redirected to login page)
let authToken = this.authService.GetAuthToken()
if(!authToken){
throw new Error("no auth token found")
}
return this.pouchDb
}
//TODO: now that we've moved to remote-first database, we can refactor and simplify this function significantly.
public async IsAuthenticated(): Promise<boolean> {
//parse the authToken to get user information
this.current_user = this.authService.GetCurrentUser()
try{
//lets see if we have an active session to the remote database.
await this.GetSessionDB()
if(!this.pouchDb){
console.warn("could not determine database from session info, logging out")
return false
// add JWT bearer token header to all requests
// https://stackoverflow.com/questions/62129654/how-to-handle-jwt-authentication-with-rxdb
this.pouchDb = new PouchDB(this.getRemoteUserDb(this.current_user), {
fetch: function (url, opts) {
opts.headers.set('Authorization', `Bearer ${authToken}`)
return PouchDB.fetch(url, opts);
}
let session = await this.pouchDb.getSession()
let authUser = session?.userCtx?.name
let isAuth = !!authUser
console.warn("IsAuthenticated? getSession() ====> ", isAuth)
return isAuth;
} catch (e) {
return false
}
})
return this.pouchDb
}
/**
@ -198,59 +144,4 @@ export class FastenDbService extends PouchdbRepository {
return summary
}
/////////////////////////////////////////////////////////////////////////////////////////////////
//Private Methods
/////////////////////////////////////////////////////////////////////////////////////////////////
/**
* After user login:
* - set the current_user in localStorage
* - create a (new/existing) local database,
* - enable indexes
* - enable sync
* - (TODO) enable encryption
* @param userIdentifier
* @constructor
*/
private async postLoginHook(userIdentifier: string, pouchDb: PouchDB.Database): Promise<void> {
await this.Close();
this.pouchDb = pouchDb;
//create any necessary indexes
// this index allows us to group by source_resource_type
console.log("DB createIndex started...")
//todo, we may need to wait a couple of moments before starting this index, as the database may not exist yet (even after user creation)
await (new Promise((resolve) => setTimeout(resolve, 500))) //sleep for 0.5s.
const createIndexMsg = await this.pouchDb.createIndex({
index: {fields: [
'doc_type',
'source_resource_type',
]}
});
console.log("DB createIndex complete", createIndexMsg)
// if(sync){
// console.log("DB sync init...", userIdentifier, this.getRemoteUserDb(userIdentifier))
//
// // this.enableSync(userIdentifier)
// // .on('paused', function (info) {
// // // replication was paused, usually because of a lost connection
// // console.warn("replication was paused, usually because of a lost connection", info)
// // }).on('active', function (info) {
// // // replication was resumed
// // console.warn("replication was resumed", info)
// // }).on('error', function (err) {
// // // totally unhandled error (shouldn't happen)
// // console.error("replication unhandled error (shouldn't happen)", err)
// // });
// console.log("DB sync enabled")
//
// }
console.warn( "Configured PouchDB database for,", this.pouchDb.name );
return
}
}

View File

@ -7,19 +7,21 @@ import {ToastService} from '../services/toast.service';
import {ToastNotification, ToastType} from '../models/fasten/toast';
import {FastenDbService} from '../services/fasten-db.service';
import {environment} from '../../environments/environment';
import {AuthService} from '../services/auth.service';
@Injectable({
providedIn: 'root'
})
export class QueueService {
constructor(private toastService: ToastService, private fastenDbService: FastenDbService) { }
constructor(private toastService: ToastService, private authService: AuthService) { }
runSourceSyncWorker(source: Source):Observable<string> {
if (typeof Worker !== 'undefined') {
const sourceSync = new SourceSyncMessage()
sourceSync.source = source
sourceSync.current_user = this.fastenDbService.current_user
sourceSync.current_user = this.authService.GetCurrentUser()
sourceSync.auth_token = this.authService.GetAuthToken()
sourceSync.couchdb_endpoint_base = environment.couchdb_endpoint_base
sourceSync.fasten_api_endpoint_base = environment.fasten_api_endpoint_base
const input$: Observable<string> = of(JSON.stringify(sourceSync));

View File

@ -20,7 +20,7 @@ export class SourceSyncWorker implements DoWork<string, string> {
console.log(msg); // outputs 'Hello from main thread'
const sourceSyncMessage = JSON.parse(msg) as SourceSyncMessage
const db = NewPouchdbRepositoryWebWorker(sourceSyncMessage.current_user, sourceSyncMessage.couchdb_endpoint_base)
const db = NewPouchdbRepositoryWebWorker({current_user: sourceSyncMessage.current_user, auth_token: sourceSyncMessage.auth_token}, sourceSyncMessage.couchdb_endpoint_base)
let clientConfig = new ClientConfig()
clientConfig.fasten_api_endpoint_base = sourceSyncMessage.fasten_api_endpoint_base

View File

@ -51,9 +51,10 @@ import {utils} from 'protractor';
* @constructor
*/
export function NewPouchdbRepositoryWebWorker(current_user: string, couchDbEndpointBase: string, localPouchDb?: PouchDB.Database): PouchdbRepository {
export function NewPouchdbRepositoryWebWorker(auth: {current_user: string, auth_token: string}, couchDbEndpointBase: string, localPouchDb?: PouchDB.Database): PouchdbRepository {
let pouchdbRepository = new PouchdbRepository(couchDbEndpointBase, localPouchDb)
pouchdbRepository.current_user = current_user
pouchdbRepository.current_user = auth.current_user
pouchdbRepository._auth_token = auth.auth_token
return pouchdbRepository
}
export class PouchdbRepository implements IDatabaseRepository {
@ -62,18 +63,20 @@ export class PouchdbRepository implements IDatabaseRepository {
remotePouchEndpoint: string // "http://localhost:5984"
pouchDb: PouchDB.Database
current_user: string
_auth_token: string
//encryption configuration
cryptConfig: PouchdbCryptConfig = null
encryptionInitComplete: boolean = false
/**
* This class can be initialized in 2 states
* - unauthenticated
* - authenticated - determined using cookie and localStorage.current_user
* @param userIdentifier
* @param encryptionKey
*/
// There are 3 different ways to initialize the Database
// - explicitly after signin/signup (not supported by this class, see FastenDbService)
// - explicitly during web-worker init
// - implicitly after Lighthouse redirect (not supported by this class, see FastenDbService)
// Three peices of information are required during intialization
// - couchdb endpoint (constant, see environment.couchdb_endpoint_base)
// - username
// - JWT token
constructor(couchDbEndpointBase: string, localPouchDb?: PouchDB.Database) {
// couchDbEndpointBase could be a relative or absolute path.
//if its absolute, we should pass it in, as-is
@ -421,7 +424,20 @@ export class PouchdbRepository implements IDatabaseRepository {
if(!this.current_user){
throw new Error("current user is required when initializing pouchdb within web-worker")
}
this.pouchDb = new PouchDB(this.getRemoteUserDb(this.current_user), {skip_setup: true})
if(!this._auth_token){
throw new Error("auth token is required when initializing pouchdb within web-worker")
}
let auth_token = this._auth_token
// add JWT bearer token header to all requests
// https://stackoverflow.com/questions/62129654/how-to-handle-jwt-authentication-with-rxdb
this.pouchDb = new PouchDB(this.getRemoteUserDb(this.current_user), {
skip_setup: true,
fetch: function (url, opts) {
opts.headers.set('Authorization', `Bearer ${auth_token}`)
return PouchDB.fetch(url, opts);
}
})
return this.pouchDb
}

View File

@ -5099,6 +5099,11 @@ jest-worker@^27.4.5:
merge-stream "^2.0.0"
supports-color "^8.0.0"
jose@^4.10.4:
version "4.10.4"
resolved "https://registry.yarnpkg.com/jose/-/jose-4.10.4.tgz#5f934b2fcf2995776e8f671f7523c6ac52c138f7"
integrity sha512-eBH77Xs9Yc/oTDvukhAEDVMijhekPuNktXJL4tUlB22jqKP1k48v5nmsUmc8feoJPsxB3HsfEt2LbVSoz+1mng==
jose@^4.6.0:
version "4.9.3"
resolved "https://registry.yarnpkg.com/jose/-/jose-4.9.3.tgz#890abd3f26725fe0f2aa720bc2f7835702b624db"

1
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/gin-gonic/gin v1.8.1
github.com/go-kivik/couchdb/v3 v3.3.0
github.com/go-kivik/kivik/v3 v3.2.3
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/golang/mock v1.4.4
github.com/sirupsen/logrus v1.9.0
github.com/spf13/viper v1.12.0

2
go.sum
View File

@ -96,6 +96,8 @@ github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=