mirror of
https://github.com/immich-app/immich.git
synced 2025-01-07 00:50:23 -05:00
fix(server): mobile oauth login (#13474)
This commit is contained in:
parent
7e49b0c875
commit
4c55597478
3 changed files with 60 additions and 10 deletions
|
@ -17,6 +17,8 @@ const authServer = {
|
|||
external: 'http://127.0.0.1:3000',
|
||||
};
|
||||
|
||||
const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redirect';
|
||||
|
||||
const redirect = async (url: string, cookies?: string[]) => {
|
||||
const { headers } = await request(url)
|
||||
.get('/')
|
||||
|
@ -24,8 +26,8 @@ const redirect = async (url: string, cookies?: string[]) => {
|
|||
return { cookies: (headers['set-cookie'] as unknown as string[]) || [], location: headers.location };
|
||||
};
|
||||
|
||||
const loginWithOAuth = async (sub: OAuthUser | string) => {
|
||||
const { url } = await startOAuth({ oAuthConfigDto: { redirectUri: `${baseUrl}/auth/login` } });
|
||||
const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) => {
|
||||
const { url } = await startOAuth({ oAuthConfigDto: { redirectUri: redirectUri ?? `${baseUrl}/auth/login` } });
|
||||
|
||||
// login
|
||||
const response1 = await redirect(url.replace(authServer.internal, authServer.external));
|
||||
|
@ -255,4 +257,50 @@ describe(`/oauth`, () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mobile redirect override', () => {
|
||||
beforeAll(async () => {
|
||||
await setupOAuth(admin.accessToken, {
|
||||
enabled: true,
|
||||
clientId: OAuthClient.DEFAULT,
|
||||
clientSecret: OAuthClient.DEFAULT,
|
||||
buttonText: 'Login with Immich',
|
||||
storageLabelClaim: 'immich_username',
|
||||
mobileOverrideEnabled: true,
|
||||
mobileRedirectUri: mobileOverrideRedirectUri,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the mobile redirect uri', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/oauth/authorize')
|
||||
.send({ redirectUri: 'app.immich:///oauth-callback' });
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual({ url: expect.stringContaining(`${authServer.internal}/auth?`) });
|
||||
|
||||
const params = new URL(body.url).searchParams;
|
||||
expect(params.get('client_id')).toBe('client-default');
|
||||
expect(params.get('response_type')).toBe('code');
|
||||
expect(params.get('redirect_uri')).toBe(mobileOverrideRedirectUri);
|
||||
expect(params.get('state')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should auto register the user by default', async () => {
|
||||
const url = await loginWithOAuth('oauth-mobile-override', 'app.immich:///oauth-callback');
|
||||
expect(url).toEqual(expect.stringContaining(mobileOverrideRedirectUri));
|
||||
|
||||
// simulate redirecting back to mobile app
|
||||
const redirectUri = url.replace(mobileOverrideRedirectUri, 'app.immich:///oauth-callback');
|
||||
|
||||
const { status, body } = await request(app).post('/oauth/callback').send({ url: redirectUri });
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
accessToken: expect.any(String),
|
||||
isAdmin: false,
|
||||
name: 'OAuth User',
|
||||
userEmail: 'oauth-mobile-override@immich.app',
|
||||
userId: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -50,6 +50,7 @@ const getClaims = (sub: string) => claims.find((user) => user.sub === sub) || wi
|
|||
const setup = async () => {
|
||||
const { privateKey, publicKey } = await generateKeyPair('RS256');
|
||||
|
||||
const redirectUris = ['http://127.0.0.1:2285/auth/login', 'https://photos.immich.app/oauth/mobile-redirect'];
|
||||
const port = 3000;
|
||||
const host = '0.0.0.0';
|
||||
const oidc = new Provider(`http://${host}:${port}`, {
|
||||
|
@ -86,14 +87,14 @@ const setup = async () => {
|
|||
{
|
||||
client_id: OAuthClient.DEFAULT,
|
||||
client_secret: OAuthClient.DEFAULT,
|
||||
redirect_uris: ['http://127.0.0.1:2285/auth/login'],
|
||||
redirect_uris: redirectUris,
|
||||
grant_types: ['authorization_code'],
|
||||
response_types: ['code'],
|
||||
},
|
||||
{
|
||||
client_id: OAuthClient.RS256_TOKENS,
|
||||
client_secret: OAuthClient.RS256_TOKENS,
|
||||
redirect_uris: ['http://127.0.0.1:2285/auth/login'],
|
||||
redirect_uris: redirectUris,
|
||||
grant_types: ['authorization_code'],
|
||||
id_token_signed_response_alg: 'RS256',
|
||||
jwks: { keys: [await exportJWK(publicKey)] },
|
||||
|
@ -101,7 +102,7 @@ const setup = async () => {
|
|||
{
|
||||
client_id: OAuthClient.RS256_PROFILE,
|
||||
client_secret: OAuthClient.RS256_PROFILE,
|
||||
redirect_uris: ['http://127.0.0.1:2285/auth/login'],
|
||||
redirect_uris: redirectUris,
|
||||
grant_types: ['authorization_code'],
|
||||
userinfo_signed_response_alg: 'RS256',
|
||||
jwks: { keys: [await exportJWK(publicKey)] },
|
||||
|
|
|
@ -188,13 +188,13 @@ export class AuthService extends BaseService {
|
|||
throw new BadRequestException('OAuth is not enabled');
|
||||
}
|
||||
|
||||
const url = await this.oauthRepository.authorize(oauth, dto.redirectUri);
|
||||
const url = await this.oauthRepository.authorize(oauth, this.resolveRedirectUri(oauth, dto.redirectUri));
|
||||
return { url };
|
||||
}
|
||||
|
||||
async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) {
|
||||
const { oauth } = await this.getConfig({ withCache: false });
|
||||
const profile = await this.oauthRepository.getProfile(oauth, dto.url, this.normalize(oauth, dto.url.split('?')[0]));
|
||||
const profile = await this.oauthRepository.getProfile(oauth, dto.url, this.resolveRedirectUri(oauth, dto.url));
|
||||
const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = oauth;
|
||||
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
||||
let user = await this.userRepository.getByOAuthId(profile.sub);
|
||||
|
@ -257,7 +257,7 @@ export class AuthService extends BaseService {
|
|||
const { sub: oauthId } = await this.oauthRepository.getProfile(
|
||||
oauth,
|
||||
dto.url,
|
||||
this.normalize(oauth, dto.url.split('?')[0]),
|
||||
this.resolveRedirectUri(oauth, dto.url),
|
||||
);
|
||||
const duplicate = await this.userRepository.getByOAuthId(oauthId);
|
||||
if (duplicate && duplicate.id !== auth.user.id) {
|
||||
|
@ -369,10 +369,11 @@ export class AuthService extends BaseService {
|
|||
return options.isValid(value) ? (value as T) : options.default;
|
||||
}
|
||||
|
||||
private normalize(
|
||||
private resolveRedirectUri(
|
||||
{ mobileRedirectUri, mobileOverrideEnabled }: { mobileRedirectUri: string; mobileOverrideEnabled: boolean },
|
||||
redirectUri: string,
|
||||
url: string,
|
||||
) {
|
||||
const redirectUri = url.split('?')[0];
|
||||
const isMobile = redirectUri.startsWith('app.immich:/');
|
||||
if (isMobile && mobileOverrideEnabled && mobileRedirectUri) {
|
||||
return mobileRedirectUri;
|
||||
|
|
Loading…
Reference in a new issue