Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EAP-PEAP-GTC/MSCHAPv2 with local SQLite password backend #12

Merged
merged 6 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/common/api/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as t from "io-ts"

import { RadiusUserPasswordStatusType, RadiusUserPasswords, RadiusUserType } from "../types/users/RadiusUser"

export const CreateOrUpdateUserRequestType = t.partial({})

export type CreateOrUpdateUserRequest = t.TypeOf<typeof CreateOrUpdateUserRequestType>

export const ListUserResponseType = t.readonlyArray(RadiusUserType)

export type ListUserResponse = t.TypeOf<typeof ListUserResponseType>

export const ListUserPasswordStatusResponseType = t.readonlyArray(RadiusUserPasswordStatusType)

export const UpdateUserPasswordsRequestType: t.Type<Partial<RadiusUserPasswords>> = t.partial({
clearText: t.union([t.string, t.null]),
ntHash: t.union([t.string, t.null]),
ssha: t.union([t.string, t.null]),
ssha512: t.union([t.string, t.null]),
})

export type UpdateUserPasswordsRequest = t.TypeOf<typeof UpdateUserPasswordsRequestType>
59 changes: 59 additions & 0 deletions packages/common/types/users/RadiusUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as t from "io-ts"

import { Username, UsernameType } from "./Username"

export interface RadiusUser {
username: Username
}

interface EncodedRadiusUser {
username: string
}

export const RadiusUserType: t.Type<RadiusUser, EncodedRadiusUser> = t.type({
username: UsernameType,
})

/**
* For each password attribute,
* string = password hash for this method
* null = method is disabled
*/
export interface RadiusUserPasswords {
username: Username
clearText: string | null
ntHash: string | null
ssha512: string | null
}

interface EncodedRadiusUserPasswords {
username: string
clearText: string | null
ntHash: string | null
ssha512: string | null
}

export const RadiusUserPasswordsType: t.Type<RadiusUserPasswords, EncodedRadiusUserPasswords> = t.type({
username: UsernameType,
clearText: t.union([t.string, t.null]),
ntHash: t.union([t.string, t.null]),
ssha512: t.union([t.string, t.null]),
})

/**
* Indicates whether a user has a password set for each method
*/
export type RadiusUserPasswordStatus = {
[K in keyof RadiusUserPasswords]: K extends "username" ? Username : boolean
}

type EncodedRadiusUserPasswordStatus = {
[K in keyof RadiusUserPasswordStatus]: RadiusUserPasswords[K] extends Username ? string : boolean
}

export const RadiusUserPasswordStatusType: t.Type<RadiusUserPasswordStatus, EncodedRadiusUserPasswordStatus> = t.type({
username: UsernameType,
clearText: t.boolean,
ntHash: t.boolean,
ssha512: t.boolean,
})
24 changes: 24 additions & 0 deletions packages/common/types/users/Username.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as E from "fp-ts/lib/Either"
import * as F from "fp-ts/lib/function"
import * as t from "io-ts"

const MAX_USERNAME_LENGTH = 64

const USERNAME_REGEX = /^[a-zA-Z0-9_-]+$/

function isUsername(u: string): boolean {
return u.length > 0 && u.length <= MAX_USERNAME_LENGTH && USERNAME_REGEX.test(u)
}

export type Username = t.Branded<string, { readonly Username: unique symbol }>

export const UsernameType = new t.Type<Username, string, unknown>(
"Username",
(u): u is Username => typeof u === "string" && isUsername(u),
(u, c) =>
F.pipe(
t.string.validate(u, c),
E.chain((u) => (isUsername(u) ? t.success(u as Username) : t.failure(u, c))),
),
t.identity,
)
11 changes: 10 additions & 1 deletion packages/supervisor/src/api/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@ import { MPSKController } from "./mpsks.controller"
import { PkiController } from "./pki.controller"
import { RadiusdController } from "./radiusd.controller"
import { RlmRestController } from "./rlm_rest.controller"
import { RadiusUserController, RadiusUserPasswordController } from "./users.controller"
import { ConfigModule } from "../config"
import { PkiModule } from "../pki/module"
import { RadiusdModule } from "../radiusd/module"
import { RlmRestModule } from "../rlm_rest/module"
import { StorageModule } from "../storages/module"

@Module({
controllers: [MPSKController, PkiController, RadiusClientController, RadiusdController, RlmRestController],
controllers: [
MPSKController,
PkiController,
RadiusClientController,
RadiusdController,
RadiusUserController,
RadiusUserPasswordController,
RlmRestController,
],
imports: [ConfigModule, PkiModule, RadiusdModule, RlmRestModule, StorageModule],
providers: [ResponseInterceptor],
})
Expand Down
41 changes: 35 additions & 6 deletions packages/supervisor/src/api/rlm_rest.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
InternalServerErrorException,
NotFoundException,
Post,
forwardRef,
} from "@nestjs/common"
import { Client } from "@yonagi/common/types/Client"
import { NameType } from "@yonagi/common/types/Name"
import { CallingStationIdAuthentication } from "@yonagi/common/types/mpsks/MPSK"
import { RadiusUserPasswords } from "@yonagi/common/types/users/RadiusUser"
import { mapValidationLeftError } from "@yonagi/common/utils/Either"
import { getOrThrow, tryCatchF } from "@yonagi/common/utils/TaskEither"
import * as TE from "fp-ts/lib/TaskEither"
Expand All @@ -28,13 +28,19 @@ import {
RlmRestMacAuthResponse,
RlmRestMacAuthResponseType,
} from "../rlm_rest/types/macAuth"
import { AbstractMPSKStorage } from "../storages"
import {
RlmRestPasswordAuthRequestType,
RlmRestPasswordAuthResponse,
RlmRestPasswordAuthResponseType,
} from "../rlm_rest/types/passwordAuth"
import { AbstractMPSKStorage, AbstractRadiusUserPasswordStorage } from "../storages"

@Controller("/api/v1/rlm_rest")
export class RlmRestController {
constructor(
@Inject(forwardRef(() => AbstractMPSKStorage)) private readonly mpskStorage: AbstractMPSKStorage,
@Inject(forwardRef(() => DynamicClientResolver)) private readonly clientResolver: DynamicClientResolver,
@Inject(AbstractMPSKStorage) private readonly mpskStorage: AbstractMPSKStorage,
@Inject(DynamicClientResolver) private readonly clientResolver: DynamicClientResolver,
@Inject(AbstractRadiusUserPasswordStorage) private readonly passwordStorage: AbstractRadiusUserPasswordStorage,
) {}

@Post("/mac/authorize")
Expand All @@ -58,7 +64,7 @@ export class RlmRestController {
@Post("/clients/authorize")
@EncodeResponseWith(RlmRestClientAuthResponseType)
async authorizeClient(@Body() rawBody: unknown): Promise<RlmRestClientAuthResponse> {
const o = await F.pipe(
return await F.pipe(
TE.Do,
TE.bindW("request", () => TE.fromEither(validateRequestParam(rawBody, RlmRestClientAuthRequestType))),
TE.bindW("client", ({ request: { clientIpAddr } }) =>
Expand All @@ -83,6 +89,29 @@ export class RlmRestController {
TE.map(({ name, client: { secret } }) => ({ name, secret })),
getOrThrow(),
)()
return o
}

@Post("/passwords/authorize")
@EncodeResponseWith(RlmRestPasswordAuthResponseType)
async authorizePassword(@Body() rawBody: unknown): Promise<RlmRestPasswordAuthResponse> {
return await F.pipe(
TE.fromEither(validateRequestParam(rawBody, RlmRestPasswordAuthRequestType)),
tryCatchF(
({ username }) => this.passwordStorage.getByUsername(username),
(reason) => new InternalServerErrorException(reason),
),
TE.filterOrElseW(
(password): password is RadiusUserPasswords => password !== null,
() => new NotFoundException("No password found"),
),
TE.map(
({ clearText, ntHash, ssha512 }): RlmRestPasswordAuthResponse => ({
cleartext: clearText ?? undefined,
nt: ntHash ?? undefined,
ssha512: ssha512 ?? undefined,
}),
),
getOrThrow(),
)()
}
}
108 changes: 108 additions & 0 deletions packages/supervisor/src/api/users.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
BadRequestException,
Body,
Controller,
Delete,
Get,
Inject,
Param,
Post,
UseInterceptors,
} from "@nestjs/common"
import {
CreateOrUpdateUserRequestType,
ListUserPasswordStatusResponseType,
ListUserResponse,
UpdateUserPasswordsRequestType,
} from "@yonagi/common/api/users"
import { RadiusUserPasswordStatus } from "@yonagi/common/types/users/RadiusUser"
import { UsernameType } from "@yonagi/common/types/users/Username"
import { mapValidationLeftError } from "@yonagi/common/utils/Either"
import { getOrThrow, tryCatchF } from "@yonagi/common/utils/TaskEither"
import * as E from "fp-ts/lib/Either"
import * as TE from "fp-ts/lib/TaskEither"
import * as F from "fp-ts/lib/function"

import { ResponseInterceptor } from "./api.middleware"
import { EncodeResponseWith } from "./common"
import { AbstractRadiusUserPasswordStorage, AbstractRadiusUserStorage } from "../storages"

@Controller("/api/v1/users")
@UseInterceptors(ResponseInterceptor)
export class RadiusUserController {
@Get("/")
async all(): Promise<ListUserResponse> {
return await this.storage.all()
}

@Post("/:username")
async createOrUpdate(@Param("username") unknownUsername: unknown, @Body() u: unknown): Promise<void> {
await F.pipe(
TE.fromEither(
F.pipe(
E.Do,
E.bindW("username", () => UsernameType.decode(unknownUsername)),
E.bindW("form", () => CreateOrUpdateUserRequestType.decode(u)),
mapValidationLeftError((e) => new BadRequestException(String(e))),
),
),
tryCatchF(
({ username, form }) => this.storage.createOrUpdate(username, { username, ...form }),
(reason) => new Error(String(reason)),
),
getOrThrow(),
)()
}

@Delete("/:username")
async delete(@Param("username") unknownUsername: unknown): Promise<void> {
await F.pipe(
TE.fromEither(
F.pipe(
UsernameType.decode(unknownUsername),
mapValidationLeftError((e) => new Error(String(e))),
),
),
tryCatchF(
(username) => this.storage.deleteByUsername(username),
(reason) => new Error(String(reason)),
),
getOrThrow(),
)()
}

constructor(@Inject(AbstractRadiusUserStorage) private readonly storage: AbstractRadiusUserStorage) {}
}

@Controller("/api/v1/passwords")
@UseInterceptors(ResponseInterceptor)
export class RadiusUserPasswordController {
@Get("/")
@EncodeResponseWith(ListUserPasswordStatusResponseType)
async all(): Promise<readonly RadiusUserPasswordStatus[]> {
return await this.storage.allStatus()
}

@Post("/:username")
async update(@Param("username") unknownUsername: unknown, @Body() u: unknown): Promise<void> {
await F.pipe(
TE.fromEither(
F.pipe(
E.Do,
E.bindW("username", () => UsernameType.decode(unknownUsername)),
E.bindW("form", () => UpdateUserPasswordsRequestType.decode(u)),
mapValidationLeftError((e) => new BadRequestException(String(e))),
),
),
tryCatchF(
({ username, form }) => this.storage.createOrUpdate(username, form),
(reason) => new Error(String(reason)),
),
getOrThrow(),
)()
}

constructor(
@Inject(AbstractRadiusUserPasswordStorage) private readonly storage: AbstractRadiusUserPasswordStorage,
) {}
}
12 changes: 4 additions & 8 deletions packages/supervisor/src/configs/raddb/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { generateClients } from "./clients"
import { generateEapModule } from "./modules/eapModule"
import { generateRestModule } from "./modules/restModule"
import { generateModules } from "./modules"
import { patchAuthLogEnable } from "./radiusdConfig"
import { generateDefaultSite } from "./sites/defaultSite"
import { generateDynamicClientSite } from "./sites/dynClientSite"
import { generateSites } from "./sites"
import { RaddbGenParams } from ".."

export async function generateRaddb(params: RaddbGenParams): Promise<void> {
await Promise.all([
generateClients(params),
generateDynamicClientSite(params),
generateDefaultSite(params),
generateEapModule(params),
generateRestModule(params),
generateModules(params),
generateSites(params),
patchAuthLogEnable(params),
])
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ export async function generateEapModule({ pki, raddbPath }: RaddbGenParams): Pro
await writeFile(
configPath,
dedent(`
eap {
default_eap_type = tls
eap outer-eap {
default_eap_type = peap
type = peap
type = tls

timer_expire = 60
max_sessions = \${max_requests}

Expand Down Expand Up @@ -57,6 +60,27 @@ export async function generateEapModule({ pki, raddbPath }: RaddbGenParams): Pro
tls {
tls = tls-common
}

peap {
tls = tls-common
default_eap_type = mschapv2
virtual_server = "inner-tunnel"
copy_request_to_tunnel = yes
}
}

eap inner-eap {
default_eap_type = gtc
type = gtc
type = mschapv2

gtc {
auth_type = PAP
}

mschapv2 {
send_error = yes
}
}
`),
)
Expand Down
8 changes: 8 additions & 0 deletions packages/supervisor/src/configs/raddb/modules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { generateEapModule } from "./eap"
import { generateMschapModule } from "./mschap"
import { generateRestModule } from "./rest"
import { RaddbGenParams } from "../.."

export async function generateModules(params: RaddbGenParams): Promise<void> {
await Promise.all([generateEapModule(params), generateMschapModule(params), generateRestModule(params)])
}
Loading
Loading