Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed Jan 23, 2024
0 parents commit f881f9a
Show file tree
Hide file tree
Showing 47 changed files with 17,675 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
.idea
web/dist
data

# mac
.DS_Store
674 changes: 674 additions & 0 deletions COPYING

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#
# Build frontend
#
FROM node:lts-alpine as frontend-builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY ./web ./web
COPY ./*.config.js .
RUN npm run build

#
# Build backend
#
FROM golang:1.21-alpine as backend-builder

ARG TARGETARCH
ARG TARGETOS

ENV GO111MODULE on
ENV GOPATH /

RUN mkdir /app && cd /app
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download && go mod verify

COPY . .
COPY --from=frontend-builder /app/web/dist /app/web/dist
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s" -o /app/nuts-admin

#
# Runtime
#
FROM alpine:3.18
RUN mkdir /app && cd /app
WORKDIR /app
COPY --from=backend-builder /app/nuts-admin .
HEALTHCHECK --start-period=5s --timeout=5s --interval=5s \
CMD wget --no-verbose --tries=1 --spider http://localhost:1305/status || exit 1
EXPOSE 1305
ENTRYPOINT ["/app/nuts-admin"]
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# nuts-admin
Application which shows how to integrate with the Nuts node to administer identities.

## Building and running

### Development

During front-end development, you probably want to use the real filesystem and webpack in watch mode:

```shell
make dev
```

The API and domain types are generated from the `api/api.yaml`.
```shell
make gen-api
```

### Docker
```shell
$ docker run -p 1305:1305 nutsfoundation/nuts-admin
```

## Configuration
When running in Docker without a config file mounted at `/app/config.yaml` it will use the default configuration.
In this case the default username will be `demo@nuts.nl`. The password is generated and printed in the log on startup.

The `node.auth.keyfile` config parameter should point to a PEM encoded private key file. The corresponding public key should be configured on the Nuts node in SSH authorized keys format.
`node.auth.user` Is required when using Nuts node API token security. It must match the user in the SSH authorized keys file.

## Technology Stack

Frontend framework is vue.js 3.x

Icons are from https://heroicons.com

CSS framework is https://tailwindcss.com
58 changes: 58 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package api

import (
"github.com/nuts-foundation/nuts-admin/identity"
"net/http"

"github.com/labstack/echo/v4"
)

var _ ServerInterface = (*Wrapper)(nil)

type Wrapper struct {
Auth auth
Identity identity.Service
}

func (w Wrapper) GetIdentities(ctx echo.Context) error {
identities, err := w.Identity.List(ctx.Request().Context())
if err != nil {
return err
}
return ctx.JSON(http.StatusOK, identities)
}

func (w Wrapper) CreateIdentity(ctx echo.Context) error {
identityRequest := CreateIdentityJSONRequestBody{}
if err := ctx.Bind(&identityRequest); err != nil {
return err
}
id, err := w.Identity.Create(ctx.Request().Context(), identityRequest.DidQualifier)
if err != nil {
return err
}
return ctx.JSON(http.StatusOK, id)
}

func (w Wrapper) CheckSession(ctx echo.Context) error {
// If this function is reached, it means the session is still valid
return ctx.NoContent(http.StatusNoContent)
}

func (w Wrapper) CreateSession(ctx echo.Context) error {
sessionRequest := CreateSessionRequest{}
if err := ctx.Bind(&sessionRequest); err != nil {
return err
}

if !w.Auth.CheckCredentials(sessionRequest.Username, sessionRequest.Password) {
return echo.NewHTTPError(http.StatusForbidden, "invalid credentials")
}

token, err := w.Auth.CreateJWT(sessionRequest.Username)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err)
}

return ctx.JSON(http.StatusOK, CreateSessionResponse{Token: string(token)})
}
107 changes: 107 additions & 0 deletions api/api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
openapi: 3.0.0
info:
title: Nuts Admin API
version: 1.0.0

paths:
/web/auth:
post:
operationId: createSession
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateSessionRequest"
responses:
'200':
description: A session was succesfully created
content:
application/json:
schema:
$ref: "#/components/schemas/CreateSessionResponse"
'403':
description: Invalid credentials

/web/private:
get:
description: Checks whether the current session is valid. If not, the client should authenticate before calling other API operations.
operationId: checkSession
responses:
'204':
description: The session is valid.
'400':
description: The session is invalid.

/web/private/id:
get:
operationId: getIdentities
responses:
'200':
description: List of identities
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Identity"
post:
operationId: createIdentity
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- did_qualifier
properties:
did_qualifier:
type: string
responses:
'200':
description: The identity was succesfully created
content:
application/json:
schema:
$ref: "#/components/schemas/Identity"
'400':
description: The identity could not be created


components:
schemas:
CreateSessionRequest:
required:
- username
- password
properties:
username:
type: string
example: demo@nuts.nl
password:
type: string
CreateSessionResponse:
required:
- token
properties:
token:
type: string
Identity:
type: object
description: An identity object
required:
- did
- name
properties:
did:
type: string
description: The DID associated with this identity
example:
"did:web:example.com:iam:user1"
name:
type: string
description: |
The name of this identity, which is the last path part of a did:web DID.
If the DID does not contain paths, or it is not a did:web DID, it will be the same as the DID.
example: "user1"
62 changes: 62 additions & 0 deletions api/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package api

import (
"crypto/ecdsa"
"log"
"time"

"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/jwt/openid"
)

type UserAccount struct {
Username string
Password string
}

type auth struct {
sessionKey *ecdsa.PrivateKey
userAccounts []UserAccount
}

func NewAuth(key *ecdsa.PrivateKey, userAccounts []UserAccount) auth {
return auth{
sessionKey: key,
userAccounts: userAccounts,
}
}

func (auth auth) CheckCredentials(username, password string) bool {
for _, account := range auth.userAccounts {
if account.Username == username && account.Password == password {
return true
}
}
return false
}

func (auth auth) CreateJWT(email string) ([]byte, error) {
t := openid.New()
t.Set(jwt.IssuedAtKey, time.Now())
// session is valid for 20 minutes
t.Set(jwt.ExpirationKey, time.Now().Add(20*time.Minute))
t.Set(openid.EmailKey, email)

signed, err := jwt.Sign(t, jwa.ES256, auth.sessionKey)
if err != nil {
log.Printf("failed to sign token: %s", err)
return nil, err
}
return signed, nil
}

func (auth auth) ValidateJWT(token []byte) (jwt.Token, error) {
pubKey := auth.sessionKey.PublicKey
t, err := jwt.Parse(token, jwt.WithVerify(jwa.ES256, pubKey), jwt.WithValidate(true))
if err != nil {
log.Printf("unable to parse token: %s", err)
return nil, err
}
return t, nil
}
Loading

0 comments on commit f881f9a

Please sign in to comment.