try to integrate new auth

Signed-off-by: MiniDigger <admin@benndorf.dev>
This commit is contained in:
MiniDigger 2021-10-16 12:09:44 +02:00
parent 051e6c281e
commit 733c151f5f
38 changed files with 457 additions and 539 deletions

View File

@ -33,8 +33,7 @@ services:
context: ../..
dockerfile: docker/deployment/hangar-backend/Dockerfile
environment:
SSO_SECRET: "${SSO_SECRET}"
API_KEY: "${API_KEY}"
SSO_CLIENT_ID: "${SSO_CLIENT_ID}"
POSTGRES_USER: "${POSTGRES_USER}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
TOKEN_SECRET: "${TOKEN_SECRET}"

View File

@ -3,35 +3,24 @@ spring:
url: jdbc:postgresql://hangar_new_db:5432/hangar
username: "${POSTGRES_USER}"
password: "${POSTGRES_PASSWORD}"
jackson:
serialization:
WRITE_DATES_AS_TIMESTAMPS: false
date-format: com.fasterxml.jackson.databind.util.StdDateFormat
fake-user:
enabled: false
hangar:
dev: false
auth-url: "https://hangar-auth.benndorf.dev"
base-url: "https://hangar.benndorf.dev"
plugin-upload-dir: "/hangar/uploads"
licenses:
- "MIT"
- "Apache 2.0"
- "GPL"
- "LPGL"
- "(custom)"
announcements:
-
text: "This is a staging server for testing purposes. Data could be deleted at any time. email confirmations are disabled. If you wanna help test, sneak into #hangar-dev"
color: "#ff544b"
sso:
secret: "${SSO_SECRET}"
api-key: "${API_KEY}"
auth-url: "https://hangar-auth.benndorf.dev"
oauth-url: "https://hangar-oauth.benndorf.dev"
client-id: "${SSO_CLIENT_ID}"
security:
api:
@ -39,5 +28,7 @@ hangar:
avatar-url: "https://hangar-auth.benndorf.dev/avatar/%s?size=120x120"
token-secret: "${TOKEN_SECRET}"
projects:
name-regex: "^[a-zA-Z0-9-_]{3,}$"
logging:
level:
root: INFO
org.springframework: INFO

View File

@ -10,7 +10,13 @@ services:
POSTGRES_PASSWORD: 'hangar'
ports:
- "5432:5432"
networks:
- web
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
networks:
web:
name: traefik-overlay
external: true

View File

@ -15,19 +15,19 @@ services:
working_dir: /app
depends_on:
- 'db'
- 'auth'
# - 'auth'
- 'mail'
stdin_open: true
labels:
- "traefik.enable=true"
- "traefik.http.services.hangarnew.loadbalancer.server.port=8080"
- "traefik.http.routers.hangarnew.rule=Host(`hangar-new.minidigger.me`)"
- "traefik.http.routers.hangarnew.rule=Host(`hangar.benndorf.dev`)"
- "traefik.http.routers.hangarnew.entrypoints=web-secure"
- "traefik.http.routers.hangarnew.tls=true"
- "traefik.http.routers.hangarnew.tls.options=default"
- "traefik.http.routers.hangarnew.tls.certresolver=default"
- "traefik.http.routers.hangarnew.tls.domains[0].main=minidigger.me"
- "traefik.http.routers.hangarnew.tls.domains[0].sans=*.minidigger.me"
- "traefik.http.routers.hangarnew.tls.domains[0].main=benndorf.dev"
- "traefik.http.routers.hangarnew.tls.domains[0].sans=*.benndorf.dev"
networks:
- web
user: appuser
@ -54,63 +54,18 @@ services:
- "traefik.enable=true"
- "traefik.http.services.hangar-mail.loadbalancer.server.port=8025"
- "traefik.http.routers.hangar-mail.middlewares=basicauth@file"
- "traefik.http.routers.hangar-mail.rule=Host(`hangar-mail-new.minidigger.me`)"
- "traefik.http.routers.hangar-mail.rule=Host(`hangar-mail.benndorf.dev`)"
- "traefik.http.routers.hangar-mail.entrypoints=web-secure"
- "traefik.http.routers.hangar-mail.tls=true"
- "traefik.http.routers.hangar-mail.tls.options=default"
- "traefik.http.routers.hangar-mail.tls.certresolver=default"
- "traefik.http.routers.hangar-mail.tls.domains[0].main=minidigger.me"
- "traefik.http.routers.hangar-mail.tls.domains[0].sans=*.minidigger.me"
- "traefik.http.routers.hangar-mail.tls.domains[0].main=benndorf.dev"
- "traefik.http.routers.hangar-mail.tls.domains[0].sans=*.benndorf.dev"
networks:
- web
ports:
- "8025:8025"
auth:
build:
context: ../../HangarAuth
dockerfile: Dockerfile
ports:
- "8000:8000"
depends_on:
- "db"
- "redis"
- "mail"
labels:
- "traefik.enable=true"
- "traefik.http.services.hangar-auth.loadbalancer.server.port=8000"
- "traefik.http.routers.hangar-auth.rule=Host(`hangar-auth-new.minidigger.me`)"
- "traefik.http.routers.hangar-auth.entrypoints=web-secure"
- "traefik.http.routers.hangar-auth.tls=true"
- "traefik.http.routers.hangar-auth.tls.options=default"
- "traefik.http.routers.hangar-auth.tls.certresolver=default"
- "traefik.http.routers.hangar-auth.tls.domains[0].main=minidigger.me"
- "traefik.http.routers.hangar-auth.tls.domains[0].sans=*.minidigger.me"
volumes:
- public_html:/public_html
environment:
PYTHONUNBUFFERED: 1
SECRET_KEY: "TzNc3RTpfVn1xxNV90PPGEfs7SZhy5"
EMAIL_HOST: "mail"
EMAIL_PORT: "1025"
EMAIL_SSL: "false"
EMAIL_TLS: "false"
EMAIL_HOST_USER: "dum"
EMAIL_HOST_PASSWORD: "dum"
HANGAR_HOST: "http://localhost:3000"
DB_NAME: "hangarauth"
DB_USER: "hangar"
DB_PASSWORD: "hangar"
DB_HOST: "db"
REDIS_HOST: "redis"
SSO_ENDPOINT_hangar: "{ 'sync_sso_endpoint': ('http://app:8080/api/sync_sso'), 'sso_secret': 'changeme', 'api_key': 'changeme' }"
DEBUG: "true"
DJANGO_SETTINGS_MODULE: "spongeauth.settings.prod"
networks:
- web
redis:
image: redis:6.2.1
networks:
- web
- "1025:1025"
networks:
web:

View File

@ -1,151 +1,27 @@
server:
port: 8080
spring:
freemarker:
request-context-attribute: rc
sql:
init:
platform: postgres
datasource:
url: jdbc:postgresql://db:5432/hangar
username: hangar
password: hangar
servlet:
multipart:
max-file-size: 10MB
max-request-size: 11MB
devtools:
restart:
additional-exclude: work/**
jackson:
serialization:
WRITE_DATES_AS_TIMESTAMPS: false
date-format: com.fasterxml.jackson.databind.util.StdDateFormat
logging:
level:
# root: TRACE
io:
papermc:
hangar:
config:
jackson: DEBUG
fake-user:
enabled: false
id: -2
name: paper
username: paper
email: paper@papermc.io
hangar:
dev: true
auth-url: "http://localhost:8000"
base-url: "http://localhost:3000"
plugin-upload-dir: "/uploads"
ga-code: "UA-38006759-9"
licenses:
- "MIT"
- "Apache 2.0"
- "GPL"
- "LPGL"
- "(custom)"
sponsors:
- name: Beer
image: /images/sponsors/beer.jpg
link: https://minidigger.me
- name: MiniDigger
image: /images/sponsors/minidigger.png
link: https://minidigger.me
homepage:
update-interval: 10m
channels:
max-name-len: 15
name-regex: "^[a-zA-Z0-9]+$"
color-default: cyan
name-default: "Release"
pages:
home:
name: "Home"
message: "Welcome to your new project!"
min-len: 15
max-len: 75000
name-regex: "^[a-zA-Z0-9-_ ]+$"
max-name-len: 25
min-name-len: 3
projects:
max-name-len: 25
max-pages: 50
max-channels: 5
init-load: 25
init-version-load: 10
max-desc-len: 120
file-validate: true
stale-age: 28d
check-interval: 1h
draft-expire: 1d
user-grid-page-size: 30
max-keywords: 5
unsafe-download-max-age: 10
name-regex: "^[a-zA-Z0-9-_]{3,}$"
orgs:
create-limit: 5
dummy-email-domain: "org.papermc.io"
enabled: true
users:
max-tagline-len: 100
author-page-size: 25
staff-roles:
- Hangar_Admin
- Hangar_Mod
- Hangar_Dev
sso:
enabled: true
# relative to auth-url
login-url: "/sso/"
signup-url: "/sso/signup/"
verify-url: "/sso/sudo/"
logout-url: "/accounts/logout/"
avatar-url: "/avatar/%s?size=120x120"
secret: "changeme"
api-key: "changeme"
timeout: "2s"
reset: "10m"
security:
secure: false
unsafe-download-max-age: 600000
token-issuer: "Hangar"
token-secret: "secret!"
token-expiry: 300 # seconds
refresh-token-expiry: 30 # days
api:
url: "http://auth:8000"
avatar-url: "http://localhost:8000/avatar/%s?size=120x120" # only comment in if you run auth locally
timeout: 10000
safe-download-hosts:
- "github.com"
discourse:
enabled: false
url: "http://localhost:80/"
admin-user: "admin"
api-key: "4657cc5e3096e505903b59eb789005eb3f207d0c61f62212bf929268740c1585"
jobs:
check-interval: 30s
unknown-error-timeout: 15
status-error-timeout: 5
not-available-timeout: 2
max-concurrent-jobs: 32
logging:
level:
# root: TRACE
io:
papermc:
hangar:
config:
jackson: DEBUG

View File

@ -14,7 +14,7 @@
:loading="loadings.name"
:error-messages="errorMsgs.name"
:rules="[
$util.$vc.require($t('channel.modal.name')),
$util.$vc.required($t('channel.modal.name')),
$util.$vc.regex($t('channel.modal.name'), validations.project.channels.regex),
$util.$vc.maxLength(validations.project.channels.max),
]"
@ -50,7 +50,7 @@
:label="$t('channel.modal.color')"
:error-messages="errorMsgs.color"
:loading="loadings.color"
:rules="[$util.$vc.require($t('channel.modal.color'))]"
:rules="[$util.$vc.required($t('channel.modal.color'))]"
readonly
/>
<v-checkbox v-model="form.nonReviewed" :label="$t('channel.modal.reviewQueue')" />

View File

@ -7,7 +7,7 @@
v-if="comment"
v-model.trim="commentText"
:label="$t('general.comment')"
:rules="[$util.$vc.require()]"
:rules="[$util.$vc.required()]"
hide-details
dense
:rows="2"

View File

@ -12,7 +12,7 @@
autofocus
filled
:label="label"
:rules="[$util.$vc.require(label)]"
:rules="[$util.$vc.required(label)]"
:rows="2"
auto-grow
@keydown.enter.prevent=""

View File

@ -18,7 +18,7 @@
hide-details
auto-grow
rows="2"
:rules="[$util.$vc.require()]"
:rules="[$util.$vc.required()]"
:label="$t('visibility.modal.reason')"
/>
</v-form>

View File

@ -14,7 +14,7 @@
filled
:loading="validateLoading"
:rules="[
$util.$vc.require($t('page.new.name')),
$util.$vc.required($t('page.new.name')),
$util.$vc.regex($t('page.new.name'), validations.project.pageName.regex),
$util.$vc.maxLength(validations.project.pageName.max),
$util.$vc.minLength(validations.project.pageName.min),

View File

@ -14,10 +14,10 @@
<span>{{ $t('project.flag.flagSent') }}</span>
</v-tooltip>
</template>
<v-radio-group v-model="form.selection" :rules="[$util.$vc.require('A reason')]">
<v-radio-group v-model="form.selection" :rules="[$util.$vc.required('A reason')]">
<v-radio v-for="(reason, index) in flagReasons" :key="index" :label="$t(reason.title)" :value="reason.type" />
</v-radio-group>
<v-textarea v-model.trim="form.comment" rows="3" filled :rules="[$util.$vc.require('A comment')]" :label="$t('general.comment')" />
<v-textarea v-model.trim="form.comment" rows="3" filled :rules="[$util.$vc.required('A comment')]" :label="$t('general.comment')" />
</HangarModal>
</template>

View File

@ -24,7 +24,9 @@
:placeholder="$t('version.new.form.externalUrl')"
:disabled="dep.namespace !== null && Object.keys(dep.namespace).length !== 0"
:rules="
dep.namespace !== null && Object.keys(dep.namespace).length !== 0 ? [] : [$util.$vc.require('version.new.form.externalUrl')]
dep.namespace !== null && Object.keys(dep.namespace).length !== 0
? []
: [$util.$vc.required('version.new.form.externalUrl')]
"
clearable
/>
@ -42,7 +44,7 @@
clearable
auto-select-first
:disabled="!!dep.externalUrl"
:rules="!!dep.externalUrl ? [] : [$util.$vc.require('version.new.form.hangarProject')]"
:rules="!!dep.externalUrl ? [] : [$util.$vc.required('version.new.form.hangarProject')]"
@update:search-input="onSearch($event, dep.name)"
/>
</td>
@ -63,7 +65,7 @@
hide-details
flat
:label="$t('general.name')"
:rules="[$util.$vc.require($t('general.name'))]"
:rules="[$util.$vc.required($t('general.name'))]"
:disabled="noEditing"
/>
</td>
@ -78,7 +80,7 @@
:rules="
newDep.namespace !== null && Object.keys(newDep.namespace).length !== 0
? []
: [$util.$vc.require('version.new.form.externalUrl')]
: [$util.$vc.required('version.new.form.externalUrl')]
"
clearable
/>
@ -96,7 +98,7 @@
clearable
auto-select-first
:disabled="!!newDep.externalUrl"
:rules="!!newDep.externalUrl ? [] : [$util.$vc.require('version.new.form.hangarProject')]"
:rules="!!newDep.externalUrl ? [] : [$util.$vc.required('version.new.form.hangarProject')]"
@update:search-input="onNewDepSearch($event, index)"
/>
</td>

View File

@ -1,22 +0,0 @@
import { Context } from '@nuxt/types';
export default ({ app: { $cookies }, $auth, redirect }: Context) => {
let shouldRefresh = $cookies.get('HangarAuth_REFRESH', { parseJSON: false });
if ($cookies.get('returnRoute')) {
// is returning from login
const returnRoute = $cookies.get<string>('returnRoute');
$cookies.remove('returnRoute', {
path: '/',
});
$cookies.remove('url', {
path: '/',
});
redirect(returnRoute);
// only refresh when fake user is enabled
shouldRefresh = process.env.fakeUser || false;
}
if (shouldRefresh) {
return $auth.refreshUser();
}
};

View File

@ -2,7 +2,10 @@ import { Context } from '@nuxt/types';
import { UserPermissions } from 'hangar-api';
import { AuthState } from '~/store/auth';
export default ({ store, params, $api, $auth }: Context) => {
export default async ({ store, params, $api, $auth, $cookies }: Context) => {
if ($cookies.get('HangarAuth_REFRESH', { parseJSON: false })) {
await $auth.refreshUser();
}
if (params.author && params.slug) {
if ($auth.isLoggedIn()) {
return $api

View File

@ -16,6 +16,7 @@ require('events').EventEmitter.defaultMaxListeners = 20;
require('dotenv').config();
const proxyHost = process.env.proxyHost || 'http://localhost:8080';
const oauthHost = process.env.oauthHost || 'http://localhost:4444';
const authHost = process.env.authHost || 'http://localhost:8000';
const lazyAuthHost = process.env.lazyAuthHost || 'http://localhost:8000';
const publicHost = process.env.PUBLIC_HOST || 'http://localhost:3000';
@ -55,6 +56,7 @@ export default {
env: {
proxyHost,
oauthHost,
authHost,
lazyAuthHost,
publicHost,
@ -111,7 +113,7 @@ export default {
},
router: {
middleware: ['auth', 'routePermissions'],
middleware: ['routePermissions'],
},
proxy: [

View File

@ -38,6 +38,7 @@
"nuxt-i18n": "Machine-Maker/i18n-module#fix/mixing-mem-leak",
"nuxt-property-decorator": "2.9.1",
"swagger-ui": "3.51.0",
"uuid": "8.3.2",
"vue": "2.6.14",
"vue-server-renderer": "2.6.14",
"vue-template-compiler": "2.6.14",
@ -55,6 +56,7 @@
"@types/lodash-es": "4.17.4",
"@types/lru-cache": "5.1.1",
"@types/swagger-ui-dist": "3.30.0",
"@types/uuid": "8.3.1",
"eslint": "7.29.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-nuxt": "2.0.0",
@ -63,7 +65,6 @@
"husky": "6.0.0",
"lint-staged": "11.0.0",
"prettier": "2.3.2",
"sass": "1.32.13",
"sass-loader": "10.1.1",
"typescript": "4.2.4",
"vuetify": "2.5.5",

View File

@ -52,7 +52,7 @@
filled
item-text="title"
item-value="apiName"
:rules="[$util.$vc.require()]"
:rules="[$util.$vc.required()]"
/>
</div>
<v-divider />

View File

@ -122,7 +122,7 @@
filled
:label="$t('reviews.reviewMessage')"
:rows="3"
:rules="[$util.$vc.require($t('general.message'))]"
:rules="[$util.$vc.required($t('general.message'))]"
@keydown.enter.prevent=""
/>
<v-btn block color="primary" :loading="loadingValues.send" class="mt-2" :disabled="!validForm" @click="sendMessage">

View File

@ -32,7 +32,7 @@
:autofocus="!isFile"
filled
:rules="[
$util.$vc.require($t('version.new.form.versionString')),
$util.$vc.required($t('version.new.form.versionString')),
$util.$vc.regex($t('version.new.form.versionString'), validations.version.regex),
]"
/>
@ -54,7 +54,7 @@
v-model="pendingVersion.externalUrl"
:label="$t('version.new.form.externalUrl')"
filled
:rules="[$util.$vc.require($t('version.new.form.externalUrl')), $util.$vc.url]"
:rules="[$util.$vc.required($t('version.new.form.externalUrl')), $util.$vc.url]"
/>
</v-col>
</v-row>
@ -163,7 +163,7 @@
:saveable="false"
editing
:raw="pendingVersion.description"
:rules="[$util.$vc.require($t('version.new.form.release.bulletin'))]"
:rules="[$util.$vc.required($t('version.new.form.release.bulletin'))]"
/>
</v-col>
</v-row>

View File

@ -52,7 +52,7 @@
v-model.trim="taglineForm"
:counter="validations.userTagline.max"
:label="$t('author.taglineLabel')"
:rules="[$util.$vc.require($t('author.taglineLabel')), $util.$vc.maxLength(validations.userTagline.max)]"
:rules="[$util.$vc.required($t('author.taglineLabel')), $util.$vc.maxLength(validations.userTagline.max)]"
/>
<template #other-btns>
<v-btn color="info" text :loading="loading.resetTagline" :disabled="!user.tagline" @click.stop="resetTagline">

View File

@ -13,7 +13,7 @@
counter="255"
:loading="validateLoading"
:error-messages="nameErrorMessages"
:rules="[$util.$vc.require($t('apiKeys.name')), $util.$vc.maxLength(255), $util.$vc.minLength(5)]"
:rules="[$util.$vc.required($t('apiKeys.name')), $util.$vc.maxLength(255), $util.$vc.minLength(5)]"
>
<template #append-outer>
<v-btn color="success" class="input-append-btn" :disabled="!validForm" :loading="loading" @click="create">

View File

@ -48,7 +48,7 @@
item-text="name"
item-value="userId"
:label="$t('project.new.step2.userSelect')"
:rules="[$util.$vc.require($t('project.new.step2.userSelect'))]"
:rules="[$util.$vc.required($t('project.new.step2.userSelect'))]"
:append-icon="createAsIcon"
/>
</v-col>
@ -60,7 +60,7 @@
filled
:error-messages="nameErrors"
:label="$t('project.new.step2.projectName')"
:rules="[$util.$vc.require($t('project.new.step2.projectName'))]"
:rules="[$util.$vc.required($t('project.new.step2.projectName'))]"
append-icon="mdi-form-textbox"
/>
</v-col>
@ -71,7 +71,7 @@
filled
clearable
:label="$t('project.new.step2.projectSummary')"
:rules="[$util.$vc.require($t('project.new.step2.projectSummary')), $util.$vc.maxLength(validations.project.desc.max)]"
:rules="[$util.$vc.required($t('project.new.step2.projectSummary')), $util.$vc.maxLength(validations.project.desc.max)]"
append-icon="mdi-card-text"
/>
</v-col>
@ -85,7 +85,7 @@
:label="$t('project.new.step2.projectCategory')"
item-text="title"
item-value="apiName"
:rules="[$util.$vc.require($t('project.new.step2.projectCategory'))]"
:rules="[$util.$vc.required($t('project.new.step2.projectCategory'))]"
/>
</v-col>
</v-row>

View File

@ -13,7 +13,7 @@
:loading="validateLoading"
:label="$t('organization.new.name')"
:rules="[
$util.$vc.require($t('organization.new.name')),
$util.$vc.required($t('organization.new.name')),
$util.$vc.regex($t('organization.new.name'), validations.org.regex),
$util.$vc.minLength(validations.org.min),
$util.$vc.maxLength(validations.org.max),

View File

@ -6,11 +6,6 @@ import { AuthState } from '~/store/auth';
const createAuth = ({ app: { $cookies }, $axios, store, $api, redirect }: Context) => {
class Auth {
login(redirect: string): void {
$cookies.set('returnRoute', redirect, {
path: '/',
maxAge: 120,
secure: process.env.nodeEnv === 'production',
});
location.replace(`/login?returnUrl=${process.env.publicHost}${redirect}`);
}
@ -23,7 +18,7 @@ const createAuth = ({ app: { $cookies }, $axios, store, $api, redirect }: Contex
store.commit('auth/SET_USER', null);
store.commit('auth/SET_TOKEN', null);
store.commit('auth/SET_AUTHED', false);
await $axios.get('/invalidate');
await $axios.get('/invalidate').catch(() => console.log('invalidate failed'));
$cookies.remove('HangarAuth_REFRESH', {
path: '/',
});
@ -57,7 +52,7 @@ const createAuth = ({ app: { $cookies }, $axios, store, $api, redirect }: Contex
refreshUser(): Promise<void> {
return $api.getToken(true).then((token) => {
if (token != null) {
if (token) {
if (store.state.auth.authenticated) {
return this.updateUser(token);
} else {

View File

@ -159,7 +159,7 @@ const createUtil = ({ store, error, app: { i18n } }: Context) => {
*/
hasPerms(...namedPermission: NamedPermission[]): boolean {
const perms = (store.state.auth as AuthState).routePermissions;
if (perms === null) return false;
if (!perms) return false;
const _perms: bigint = BigInt('0b' + perms);
let result = true;
for (const np of namedPermission) {
@ -239,7 +239,7 @@ const createUtil = ({ store, error, app: { i18n } }: Context) => {
}
$vc = {
require:
required:
(name: TranslateResult = 'Field') =>
(v: string) =>
!!v || i18n.t('validation.required', [name]),

View File

@ -1967,6 +1967,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
"@types/uuid@8.3.1":
version "8.3.1"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
"@types/webpack-bundle-analyzer@3.9.3":
version "3.9.3"
resolved "https://registry.yarnpkg.com/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.3.tgz#3a12025eb5d86069c30b47a157e62c0aca6e39a1"
@ -9213,7 +9218,7 @@ sass-loader@^10.2.0:
schema-utils "^3.0.0"
semver "^7.3.2"
sass@1.32.13, sass@~1.32.13:
sass@~1.32.13:
version "1.32.13"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.13.tgz#8d29c849e625a415bce71609c7cf95e15f74ed00"
integrity sha512-dEgI9nShraqP7cXQH+lEXVf73WOPCse0QlFzSD8k+1TcOxCMwVXfQlr0jtoluZysQOyJGnfr21dLvYKDJq8HkA==
@ -10485,6 +10490,11 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache@^2.0.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"

View File

@ -23,7 +23,6 @@ public class HangarConfig {
private List<Sponsor> sponsors;
private boolean dev = true;
private String authUrl;
private String pluginUploadDir = new ApplicationHome(HangarApplication.class).getDir().toPath().resolve("work").toString();
private String baseUrl;
private String gaCode = "";
@ -152,14 +151,6 @@ public class HangarConfig {
this.dev = dev;
}
public String getAuthUrl() {
return authUrl;
}
public void setAuthUrl(String authUrl) {
this.authUrl = authUrl;
}
public String getPluginUploadDir() {
return pluginUploadDir;
}

View File

@ -3,23 +3,18 @@ package io.papermc.hangar.config.hangar;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
@ConfigurationProperties(prefix = "hangar.sso")
public class SSOConfig {
// TODO weed out the useless settings
private boolean enabled = true;
private String loginUrl = "/sso/";
private String signupUrl = "/sso/signup/";
private String verifyUrl = "/sso/sudo/";
private String logoutUrl = "/accounts/logout/";
private String avatarUrl = "/avatar/%s?size=120x120";
private String secret = "changeme";
private String apiKey = "changeme";
private Duration timeout = Duration.ofSeconds(2);
private Duration reset = Duration.ofMinutes(10);
private String oauthUrl = "http://localhost:4444";
private String loginUrl = "/oauth2/auth/";
private String tokenUrl = "/oauth2/token";
private String clientId = "my-client";
private String authUrl = "http://localhost:3001";
private String signupUrl = "/account/signup";
public boolean isEnabled() {
return enabled;
@ -45,59 +40,35 @@ public class SSOConfig {
this.signupUrl = signupUrl;
}
public String getVerifyUrl() {
return verifyUrl;
public String getTokenUrl() {
return tokenUrl;
}
public void setVerifyUrl(String verifyUrl) {
this.verifyUrl = verifyUrl;
public void setTokenUrl(String tokenUrl) {
this.tokenUrl = tokenUrl;
}
public String getLogoutUrl() {
return logoutUrl;
public String getClientId() {
return clientId;
}
public void setLogoutUrl(String logoutUrl) {
this.logoutUrl = logoutUrl;
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getAvatarUrl() {
return avatarUrl;
public String getAuthUrl() {
return authUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
public void setAuthUrl(String authUrl) {
this.authUrl = authUrl;
}
public String getSecret() {
return secret;
public String getOauthUrl() {
return oauthUrl;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public Duration getTimeout() {
return timeout;
}
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
public Duration getReset() {
return reset;
}
public void setReset(Duration reset) {
this.reset = reset;
public void setOauthUrl(String oauthUrl) {
this.oauthUrl = oauthUrl;
}
}

View File

@ -1,5 +1,18 @@
package io.papermc.hangar.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.view.RedirectView;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.api.auth.RefreshResponse;
@ -13,20 +26,6 @@ import io.papermc.hangar.service.ValidationService;
import io.papermc.hangar.service.internal.auth.SSOService;
import io.papermc.hangar.service.internal.perms.roles.GlobalRoleService;
import io.papermc.hangar.service.internal.users.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.view.RedirectView;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;
@Controller
public class LoginController extends HangarComponent {
@ -62,9 +61,9 @@ public class LoginController extends HangarComponent {
}
}
@GetMapping(path = "/login", params = {"sso", "sig"})
public RedirectView loginFromAuth(@RequestParam String sso, @RequestParam String sig, @CookieValue String url, RedirectAttributes attributes) {
AuthUser authUser = ssoService.authenticate(sso, sig);
@GetMapping(path = "/login", params = {"code", "state"})
public RedirectView loginFromAuth(@RequestParam String code, @RequestParam String state, @CookieValue String url) {
AuthUser authUser = ssoService.authenticate(code, state, config.getBaseUrl() + "/login");
if (authUser == null) {
throw new HangarApiException("nav.user.error.loginFailed");
}
@ -76,8 +75,8 @@ public class LoginController extends HangarComponent {
UserTable user = userService.getOrCreate(authUser.getUserName(), authUser);
globalRoleService.removeAllGlobalRoles(user.getId());
authUser.getGlobalRoles().forEach(globalRole -> globalRoleService.addRole(globalRole.create(null, user.getId(), true)));
String token = tokenService.createTokenForUser(user);
return redirectBackOnSuccessfulLogin(url + "?token=" + token, user);
tokenService.createTokenForUser(user);
return redirectBackOnSuccessfulLogin(url);
}
@GetMapping("/refresh")
@ -95,24 +94,15 @@ public class LoginController extends HangarComponent {
}
}
// TODO needed?
@PostMapping("/verify")
public RedirectView verify(@RequestParam String returnPath) {
if (config.fakeUser.isEnabled()) {
throw new HangarApiException("nav.user.error.fakeUserEnabled", "Verififcation");
}
return redirectToSso(ssoService.getVerifyUrl(config.getBaseUrl() + returnPath));
}
@GetMapping("/signup")
public RedirectView signUp(@RequestParam(defaultValue = "") String returnUrl) {
if (config.fakeUser.isEnabled()) {
throw new HangarApiException("nav.user.error.fakeUserEnabled", "Signup");
}
return redirectToSso(ssoService.getSignupUrl(returnUrl));
return new RedirectView(ssoService.getSignupUrl(returnUrl));
}
private RedirectView redirectBackOnSuccessfulLogin(String url, UserTable user) {
private RedirectView redirectBackOnSuccessfulLogin(String url) {
if (!url.startsWith("http")) {
if (url.startsWith("/")) {
url = config.getBaseUrl() + url;

View File

@ -1,78 +0,0 @@
package io.papermc.hangar.controller.api;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.config.hangar.SSOConfig;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.internal.sso.SsoSyncData;
import io.papermc.hangar.service.internal.auth.SSOService;
import io.papermc.hangar.service.internal.users.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.Authorization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.constraints.NotEmpty;
import java.util.Map;
@Controller
@Api(tags = "Sessions (Authentication)", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@RequestMapping(path = "/api", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, method = RequestMethod.POST)
public class SSOSyncController extends HangarComponent {
private final SSOService ssoService;
private final UserService userService;
private final SSOConfig ssoConfig;
@Autowired
public SSOSyncController(SSOService ssoService, UserService userService, SSOConfig ssoConfig) {
this.ssoService = ssoService;
this.userService = userService;
this.ssoConfig = ssoConfig;
}
@ApiOperation(
value = "Syncs SSO data to Hangar",
nickname = "syncSso",
notes = "Syncs data for a user from a SpongeAuth-compatible SSO provider. The SSO provider must be provided with this endpoint, as well as with a secret and API key that matches this Hangar instance.",
authorizations = @Authorization("SSO"),
tags = "Sessions (Authentication)"
)
@ApiResponses({
@ApiResponse(code = 200, message = "Ok"),
@ApiResponse(code = 401, message = "Sent if the signature or API key missing or invalid.")
})
@PostMapping(value = "/sync_sso", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ResponseEntity<MultiValueMap<String, String>> syncSso(@RequestParam @NotEmpty String sso, @RequestParam @NotEmpty String sig, @RequestParam("api_key") @NotEmpty String apiKey) {
if (!apiKey.equals(ssoConfig.getApiKey())) {
logger.warn("SSO sync failed: bad API key ({} provided, {} expected)", apiKey, ssoConfig.getApiKey());
throw new HangarApiException(HttpStatus.BAD_REQUEST, "SSO sync failed: bad API key (" + apiKey + " provided, " + ssoConfig.getApiKey() + " expected)");
}
try {
Map<String, String> map = ssoService.decode(sso, sig);
SsoSyncData data = SsoSyncData.fromSignedPayload(map);
userService.ssoSyncUser(data);
logger.debug("SSO sync successful: {}", map);
MultiValueMap<String, String> ssoResponse = new LinkedMultiValueMap<>();
ssoResponse.set("status", "success");
return ResponseEntity.ok(ssoResponse);
} catch (SSOService.SignatureException e) {
logger.warn("SSO sync failed: invalid signature ({} for data {})", sig, sso);
throw new HangarApiException(HttpStatus.BAD_REQUEST, "SSO sync failed: invalid signature (" + sig + " for data " + sso + ")");
}
}
}

View File

@ -17,15 +17,17 @@ public class AuthUser {
private final String email;
private final String avatarUrl;
private final Locale lang;
private final String fullName;
private final List<GlobalRole> globalRoles;
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public AuthUser(long id, String userName, String email, String avatarUrl, Locale lang, String addGroups) {
public AuthUser(long id, String userName, String email, String avatarUrl, Locale lang, String fullName, String addGroups) {
this.id = id;
this.userName = userName;
this.email = email;
this.avatarUrl = avatarUrl;
this.lang = lang;
this.fullName = fullName;
if (addGroups == null || addGroups.isBlank()) {
this.globalRoles = new ArrayList<>();
} else {
@ -39,6 +41,7 @@ public class AuthUser {
this.email = email;
this.avatarUrl = "";
this.lang = Locale.ENGLISH;
this.fullName = null;
this.globalRoles = new ArrayList<>();
}
@ -66,6 +69,10 @@ public class AuthUser {
return globalRoles;
}
public String getFullName() {
return fullName;
}
@Override
public String toString() {
return "AuthUser{" +

View File

@ -0,0 +1,102 @@
package io.papermc.hangar.model.internal.sso;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Objects;
public class TokenResponce {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("expires_in")
private long expiresIn;
@JsonProperty("id_token")
private String idToken;
@JsonProperty("refresh_token")
private String refreshToken;
private String scope;
@JsonProperty("token_type")
private String tokenType;
public TokenResponce(String accessToken, long expiresIn, String idToken, String refreshToken, String scope, String tokenType) {
this.accessToken = accessToken;
this.expiresIn = expiresIn;
this.idToken = idToken;
this.refreshToken = refreshToken;
this.scope = scope;
this.tokenType = tokenType;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public long getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(long expiresIn) {
this.expiresIn = expiresIn;
}
public String getIdToken() {
return idToken;
}
public void setIdToken(String idToken) {
this.idToken = idToken;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
@Override
public String toString() {
return "TokenResponce{" +
"accessToken='" + accessToken + '\'' +
", expiresIn=" + expiresIn +
", idToken='" + idToken + '\'' +
", refreshToken='" + refreshToken + '\'' +
", scope='" + scope + '\'' +
", tokenType='" + tokenType + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TokenResponce that = (TokenResponce) o;
return expiresIn == that.expiresIn && Objects.equals(accessToken, that.accessToken) && Objects.equals(idToken, that.idToken) && Objects.equals(refreshToken, that.refreshToken) && Objects.equals(scope, that.scope) && Objects.equals(tokenType, that.tokenType);
}
@Override
public int hashCode() {
return Objects.hash(accessToken, expiresIn, idToken, refreshToken, scope, tokenType);
}
}

View File

@ -0,0 +1,137 @@
package io.papermc.hangar.model.internal.sso;
import java.util.Objects;
public class Traits {
private String email;
private String username;
private String discord;
private String github;
private String minecraft;
private String language;
private Name name;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getDiscord() {
return discord;
}
public void setDiscord(String discord) {
this.discord = discord;
}
public String getGithub() {
return github;
}
public void setGithub(String github) {
this.github = github;
}
public String getMinecraft() {
return minecraft;
}
public void setMinecraft(String minecraft) {
this.minecraft = minecraft;
}
public Name getName() {
return name;
}
public void setName(Name name) {
this.name = name;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
@Override
public String toString() {
return "Traits{" +
"email='" + email + '\'' +
", username='" + username + '\'' +
", discord='" + discord + '\'' +
", github='" + github + '\'' +
", minecraft='" + minecraft + '\'' +
", name=" + name +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Traits traits = (Traits) o;
return Objects.equals(email, traits.email) && Objects.equals(username, traits.username) && Objects.equals(discord, traits.discord) && Objects.equals(github, traits.github) && Objects.equals(minecraft, traits.minecraft) && Objects.equals(name, traits.name);
}
@Override
public int hashCode() {
return Objects.hash(email, username, discord, github, minecraft, name);
}
public static class Name {
private String first;
private String last;
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getLast() {
return last;
}
public void setLast(String last) {
this.last = last;
}
@Override
public String toString() {
return "Name{" +
"first='" + first + '\'' +
", last='" + last + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Name name = (Name) o;
return Objects.equals(first, name.first) && Objects.equals(last, name.last);
}
@Override
public int hashCode() {
return Objects.hash(first, last);
}
}
}

View File

@ -60,7 +60,7 @@ public class AuthenticationService extends HangarComponent {
public URI changeAvatarUri(String requester, String organization) throws JsonProcessingException {
ChangeAvatarToken token = getChangeAvatarToken(requester, organization);
UriComponentsBuilder uriComponents = UriComponentsBuilder.fromHttpUrl(config.getAuthUrl());
UriComponentsBuilder uriComponents = UriComponentsBuilder.fromHttpUrl(config.sso.getAuthUrl());
uriComponents.path("/accounts/user/{organization}/change-avatar/").queryParam("key", token.getSignedData());
return uriComponents.build(organization);
}
@ -69,7 +69,9 @@ public class AuthenticationService extends HangarComponent {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> bodyMap = new LinkedMultiValueMap<>();
bodyMap.add("api-key", config.sso.getApiKey());
// TODO allow changing org avatars in SSO
if (true) throw new RuntimeException("disabled");
// bodyMap.add("api-key", config.sso.getApiKey());
bodyMap.add("request_username", requester);
ChangeAvatarToken token;
token = mapper.treeToValue(restTemplate.postForObject(config.security.api.getUrl() + "/api/users/" + organization + "/change-avatar-token/", new HttpEntity<>(bodyMap, headers), ObjectNode.class), ChangeAvatarToken.class);

View File

@ -1,48 +1,48 @@
package io.papermc.hangar.service.internal.auth;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.db.dao.internal.table.auth.UserSignOnDAO;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.db.auth.UserSignOnTable;
import io.papermc.hangar.model.internal.sso.AuthUser;
import io.papermc.hangar.model.internal.sso.URLWithNonce;
import io.papermc.hangar.util.CryptoUtils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.math.BigInteger;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
// reference: https://github.com/MiniDigger/HangarAuth/blob/master/spongeauth/sso/discourse_sso.py
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.db.dao.internal.table.auth.UserSignOnDAO;
import io.papermc.hangar.model.db.auth.UserSignOnTable;
import io.papermc.hangar.model.internal.sso.AuthUser;
import io.papermc.hangar.model.internal.sso.TokenResponce;
import io.papermc.hangar.model.internal.sso.Traits;
import io.papermc.hangar.model.internal.sso.URLWithNonce;
@Service
public class SSOService {
private static final Logger LOGGER = LoggerFactory.getLogger(SSOService.class);
private final HangarConfig hangarConfig;
private final UserSignOnDAO userSignOnDAO;
private final Cache<String, String> returnUrls;
private final RestTemplate restTemplate;
@Autowired
public SSOService(HangarConfig hangarConfig, UserSignOnDAO userSignOnDAO) {
public SSOService(HangarConfig hangarConfig, UserSignOnDAO userSignOnDAO, RestTemplate restTemplate) {
this.hangarConfig = hangarConfig;
this.returnUrls = Caffeine.newBuilder().expireAfterWrite(hangarConfig.sso.getReset()).build();
this.userSignOnDAO = userSignOnDAO;
this.restTemplate = restTemplate;
}
private boolean isNonceValid(String nonce) {
@ -55,47 +55,44 @@ public class SSOService {
}
public URLWithNonce getLoginUrl(String returnUrl) {
return getUrl(returnUrl, hangarConfig.sso.getLoginUrl());
}
public URLWithNonce getSignupUrl(String returnUrl) {
return getUrl(returnUrl, hangarConfig.sso.getSignupUrl());
}
public URLWithNonce getVerifyUrl(String returnUrl) {
return getUrl(returnUrl, hangarConfig.sso.getVerifyUrl());
}
private URLWithNonce getUrl(String returnUrl, String baseUrl) {
String generatedNonce = nonce();
String payload = generatePayload( returnUrl, generatedNonce);
String sig = sign(payload);
String urlEncoded = URLEncoder.encode(payload, StandardCharsets.UTF_8);
return new URLWithNonce(String.format("%s?sso=%s&sig=%s", hangarConfig.getAuthUrl() + baseUrl, urlEncoded, sig), generatedNonce);
String url = UriComponentsBuilder.fromUriString(hangarConfig.sso.getOauthUrl() + hangarConfig.sso.getLoginUrl())
.queryParam("client_id", hangarConfig.sso.getClientId())
.queryParam("scope", "openid email profile")
.queryParam("response_type", "code")
.queryParam("redirect_uri", returnUrl)
.queryParam("state", generatedNonce)
.build().toUriString();
return new URLWithNonce(url, generatedNonce);
}
public String getSignupUrl(String returnUrl) {
// TODO figure out what we wanna do here
return hangarConfig.sso.getAuthUrl() + hangarConfig.sso.getSignupUrl();
}
private String nonce() {
return new BigInteger(130, ThreadLocalRandom.current()).toString(32);
}
private String generatePayload(String returnUrl, String nonce) {
String payload = String.format("return_sso_url=%s&nonce=%s", returnUrl, nonce);
return new String(Base64.getEncoder().encode(payload.getBytes(StandardCharsets.UTF_8)));
}
public AuthUser authenticate(String code, String nonce, String returnUrl) {
String token = redeemCode(code, returnUrl);
DecodedJWT decoded = JWT.decode(token);
public AuthUser authenticate(String payload, String sig) {
Map<String, String> decoded = decode(payload, sig);
String nonce = decoded.get("nonce");
long externalId = Long.parseLong(decoded.get("external_id"));
String username = decoded.get("username");
String email = decoded.get("email");
Traits traits = decoded.getClaim("traits").as(Traits.class);
// long externalId = Long.parseLong(decoded.get("external_id"));
// String avatarUrl = decoded.get("avatar_url");
Locale language = (traits.getLanguage() == null || traits.getLanguage().isBlank()) ? Locale.ENGLISH : Locale.forLanguageTag(traits.getLanguage());
// String addGroups = decoded.get("add_groups");
AuthUser authUser = new AuthUser(
externalId,
username,
email,
decoded.get("avatar_url"),
decoded.get("language") == null ? Locale.ENGLISH : Locale.forLanguageTag(decoded.get("language")),
decoded.get("add_groups")
-5,
traits.getUsername(),
traits.getEmail(),
"avatarUrl",
language,
traits.getName().getFirst() + traits.getName().getLast(),
"addGroups"
);
if (!isNonceValid(nonce)) {
return null;
@ -103,41 +100,24 @@ public class SSOService {
return authUser;
}
public String redeemCode(String code, String redirect) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("code", code);
map.add("grant_type", "authorization_code");
map.add("client_id", hangarConfig.sso.getClientId());
map.add("redirect_uri", redirect);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);
ResponseEntity<TokenResponce> tokenResponse = restTemplate.exchange(hangarConfig.sso.getOauthUrl() + hangarConfig.sso.getTokenUrl(), HttpMethod.POST, entity, TokenResponce.class);
return tokenResponse.getBody().getIdToken();
}
public UserSignOnTable insert(String nonce) {
return userSignOnDAO.insert(new UserSignOnTable(nonce));
}
private String sign(String payload) {
try {
return CryptoUtils.hmacSha256(hangarConfig.sso.getSecret(), payload.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
LOGGER.warn("Error while singing sso key", e);
throw new HangarApiException("nav.user.error.loginFailed");
}
}
private boolean verify(String payload, String signature) {
return sign(payload).equalsIgnoreCase(signature);
}
// reference: unsign
public Map<String, String> decode(String payload, String signature) {
if (!verify(payload, signature)) {
throw new SignatureException(payload, signature);
}
String decoded = new String(Base64.getDecoder().decode(payload));
String querystring = URLDecoder.decode(decoded, StandardCharsets.UTF_8);
return UriComponentsBuilder.fromUriString("/?" + querystring).build().getQueryParams().toSingleValueMap();
}
public String getReturnUrl(String nonce) {
return returnUrls.getIfPresent(nonce);
}
public static class SignatureException extends HangarApiException {
SignatureException(String payload, String signature) {
super("nav.user.error.hangarAuth", payload, signature);
}
}
}

View File

@ -75,7 +75,9 @@ public class OrganizationFactory extends HangarComponent {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("api-key", config.sso.getApiKey());
// TODO allow creating org users in SSO
if (true) throw new RuntimeException("disabled");
// map.add("api-key", config.sso.getApiKey());
map.add("username", name);
map.add("email", dummyEmail);
map.add("verified", Boolean.TRUE.toString());

View File

@ -106,7 +106,7 @@ public class UserService extends HangarComponent {
if (user == null) {
user = new UserTable(
authUser.getId(),
null,
authUser.getFullName(),
authUser.getUserName(),
authUser.getEmail(),
List.of(),

View File

@ -36,7 +36,7 @@ spring:
# Fake User #
#############
fake-user:
enabled: true
enabled: false
id: -2
name: paper
username: paper
@ -48,7 +48,6 @@ fake-user:
hangar:
dev: true
auth-url: "http://localhost:8000"
base-url: "http://localhost:3000"
ga-code: "UA-38006759-9"
@ -122,16 +121,13 @@ hangar:
sso:
enabled: true
# relative to auth-url
login-url: "/sso/"
signup-url: "/sso/signup/"
verify-url: "/sso/sudo/"
logout-url: "/accounts/logout/"
avatar-url: "/avatar/%s?size=120x120"
secret: "changeme"
api-key: "changeme"
timeout: "2s"
reset: "10m"
oauth-url: "http://localhost:4444"
login-url: "/oauth2/auth/"
token-url: "/oauth2/token"
client-id: "my-client"
auth-url: "http://localhost:3001"
signup-url: "/account/signup/"
security:
secure: false
@ -168,6 +164,6 @@ hangar:
logging:
level:
root: INFO
# org.springframework: DEBUG
org.springframework: DEBUG
io.papermc.hangar.service.internal.JobService: DEBUG
io.papermc.hangar.config.WebConfig.LoggingInterceptor: DEBUG