working backend changes to generate JWT tokens during signin and signup. (#9)
This commit is contained in:
parent
9d56fa2896
commit
032946100c
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
@ -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", "")
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -10,4 +10,5 @@ type DatabaseRepository interface {
|
|||
Close() error
|
||||
|
||||
CreateUser(context.Context, *models.User) error
|
||||
VerifyUser(context.Context, *models.User) error
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
//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(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
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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,41 +56,26 @@ 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")
|
||||
}
|
||||
|
||||
//parse the authToken to get user information
|
||||
this.current_user = this.authService.GetCurrentUser()
|
||||
|
||||
// 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);
|
||||
}
|
||||
})
|
||||
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> {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the crypto configuration for the authenticated user already available in the browser? Or do we need to import/generate new config.
|
||||
*/
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
1
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
Loading…
Reference in New Issue